Skip to content

[feat] 특정 사용자의 팔로워 조회 api 개발#78

Merged
buzz0331 merged 14 commits intodevelopfrom
feat/#77-get-follower
Jul 15, 2025
Merged

[feat] 특정 사용자의 팔로워 조회 api 개발#78
buzz0331 merged 14 commits intodevelopfrom
feat/#77-get-follower

Conversation

@buzz0331
Copy link
Contributor

@buzz0331 buzz0331 commented Jul 14, 2025

#️⃣ 연관된 이슈

closes #77

📝 작업 내용

특정 사용자의 팔로워 리스트를 조회하는 api의 흐름은 다음과 같습니다.

조회하려는 특정 사용자 (userId)로 User 조회
-> userId로 Following 테이블에서 팔로워 리스트 조회 -> 조회한 팔로워들의 팔로우 수 count 쿼리로 한번에 Map에 저장 -> dto 파싱

커서 기반 페이징 처리 방식
커서 기반 페이징을 위해 아래와 같이 WHERE 절에서 cursor를 기준으로 데이터를 필터링하고, 정렬하여 페이징이 가능하도록 구성했습니다.
스크린샷 2025-07-15 오전 3 05 37

동작 방식

  • 첫 요청
    클라이언트가 처음 데이터를 요청할 때는 cursor 값 없이 /users/{userId}/followers 엔드포인트를 호출합니다
    이때, 더 불러올 데이터가 존재하는 경우 응답에 nextCursor 값을 함께 포함시켜 반환합니다
  • 이후 요청
    다음 페이지가 필요할 경우, 앞서 응답으로 받은 nextCursor 값을 쿼리 파라미터로 포함하여 /users/{userId}/followers?cursor={nextCursor} 형식으로 요청을 보냅니다
    서버는 해당 cursor를 기준으로 이후 데이터를 조회하여 응답합니다.

📸 스크린샷

💬 리뷰 요구사항

N + 1 문제를 방지하기 위해 다음과 같은 두 가지 개선을 적용했습니다.

  1. 팔로워들의 팔로잉 수 조회 최적화
    Follower 리스트를 조회한 뒤, 각 Follower의 팔로잉 수를 조회하기 위해 개별 쿼리를 날리면 N + 1 문제가 발생할 수 있습니다. 이를 방지하기 위해 groupBy를 활용한 배치 쿼리를 사용하여 모든 Follower의 팔로잉 수를 한 번의 count 쿼리로 조회하고, 결과를 Map<userId, count> 형태로 메모리에 저장한 뒤, DTO를 생성할 때 해당 Map에서 값을 꺼내 사용하는 방식으로 쿼리 수를 최소화했습니다.

  2. 팔로워 정보 조회 시 Fetch Join 적용
    Following 테이블에서 Follower 정보를 조회할 때 @manytoone(fetch = FetchType.LAZY) 설정으로 인해 각 Follower에 대해 추가적인 select 쿼리가 발생해 N + 1 문제가 생깁니다. 이를 해결하기 위해 Follower 리스트를 조회할 때 User 테이블과의 fetch join을 적용하여 연관된 Follower 정보를 한 번의 쿼리로 함께 가져올 수 있도록 최적화했습니다.

📌 PR 진행 시 이러한 점들을 참고해 주세요

* P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
* P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
* P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)

Summary by CodeRabbit

  • 신규 기능

    • 사용자 팔로우/언팔로우 API가 추가되어 사용자가 다른 사용자를 팔로우하거나 언팔로우할 수 있습니다.
    • 특정 사용자의 팔로워 목록을 커서 기반 페이지네이션 방식으로 조회할 수 있는 API가 추가되었습니다.
  • 버그 수정

    • 런타임 예외 발생 시 전체 스택 트레이스를 로그에 기록하도록 개선되었습니다.
  • 테스트

    • 팔로우/언팔로우 및 팔로워 목록 조회 기능에 대한 통합 및 단위 테스트가 추가되었습니다.
  • 문서화

    • 일부 테스트 클래스의 표시 이름이 한글로 명확하게 변경되었습니다.
  • 리팩터

    • 팔로우 관련 엔티티 및 매퍼의 필드명과 메서드명이 역할에 맞게 명확히 변경되었습니다.
    • 팔로우 관계 삭제 시 물리적 삭제 대신 상태값을 변경하는 소프트 딜리트 방식이 적용되었습니다.

@coderabbitai
Copy link

coderabbitai bot commented Jul 14, 2025

"""

Walkthrough

사용자 팔로우 및 언팔로우 기능, 팔로워 조회 API가 신규 도입되었습니다. 도메인, 서비스, 어댑터, 컨트롤러, 예외, DTO, 매퍼, JPA 엔티티 등 계층별로 관련 코드가 추가 및 수정되었으며, 통합 및 단위 테스트도 함께 작성되었습니다.

Changes

파일/그룹 변경 요약
src/main/java/konkuk/thip/common/entity/BaseDomainEntity.java 상태 토글 메서드(changeStatus) 추가
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java 팔로우 관련 에러코드 추가
src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java 런타임 예외 로그에 스택트레이스 포함
src/main/java/konkuk/thip/common/util/DateUtil.java 날짜 파싱 메서드(parseDateTime) 추가
src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java
src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java
팔로우/언팔로우, 팔로워 조회 API 엔드포인트 추가
src/main/java/konkuk/thip/user/adapter/in/web/request/UserFollowRequest.java 팔로우 요청 DTO 추가
src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowResponse.java
src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java
팔로우, 팔로워 응답 DTO 추가
src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java
src/main/java/konkuk/thip/user/domain/Following.java
src/main/java/konkuk/thip/user/adapter/out/mapper/FollowingMapper.java
팔로워/팔로잉 필드명 명확화, 소프트 딜리트, 상태 관리 로직 추가
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingJpaRepository.java
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepository.java
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java
팔로우/언팔로우 저장, 상태변경, 팔로워 목록 커서 기반 조회 등 퍼시스턴스 계층 구현
src/main/java/konkuk/thip/user/application/port/in/UserFollowUsecase.java
src/main/java/konkuk/thip/user/application/port/in/UserGetFollowersUsecase.java
src/main/java/konkuk/thip/user/application/port/in/dto/UserFollowCommand.java
src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java
src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java
유스케이스, 포트, 커맨드 등 인터페이스 및 DTO 추가/확장
src/main/java/konkuk/thip/user/application/service/UserFollowService.java
src/main/java/konkuk/thip/user/application/service/UserGetFollowersService.java
팔로우/언팔로우 및 팔로워 조회 서비스 구현
src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java
src/test/java/konkuk/thip/user/adapter/in/web/UserFollowControllerTest.java
src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java
src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java
src/test/java/konkuk/thip/user/domain/FollowingTest.java
팔로우/언팔로우, 팔로워 조회 관련 통합 및 단위 테스트 추가
src/test/java/konkuk/thip/common/util/TestEntityFactory.java 팔로워/팔로잉 파라미터명 명확화
src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java 팔로우 데이터 삭제 방식 변경(deleteAllInBatch)
src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java
src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java
src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java
테스트 클래스 DisplayName 한글화 등 사소한 수정

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)
Loading
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 등)
Loading

Assessment against linked issues

Objective Addressed Explanation
팔로워 조회 API 개발 및 구현 (#77)
팔로워 조회 API 관련 DTO/서비스/포트/어댑터/테스트 작성 (#77)

Suggested labels

🐶 희진

Poem

(\(\
( -.-)
o_(")(")

팔로우 토글, 팔로워 쏙쏙,
토끼 귀 쫑긋, 코드가 톡톡!
새 API에 테스트까지,
깡총깡총 즐거운 개발의 봄.

🥕
"""


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 322b944 and 951f262.

📒 Files selected for processing (1)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java
⏰ 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
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: 예외 처리 로직을 개선할 수 있습니다.

메서드 구현 자체는 올바르지만, 예외 처리에서 개선할 점이 있습니다:

  1. 예외 메시지에 원본 예외 객체를 직접 포함하면 메시지가 복잡해질 수 있습니다.
  2. 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: 팔로우 상태 변경 로직이 올바르게 구현되었습니다.

비즈니스 로직이 명확하고 예외 처리가 적절합니다. 다만 몇 가지 개선 사항이 있습니다:

  1. type 변수명이 의미가 불명확합니다. isFollowing 또는 followAction 등으로 명명하는 것이 좋겠습니다.
  2. 주석이 한국어로 되어 있어 영어 주석으로 통일하는 것을 고려해보세요.
-        Boolean type = followCommand.type();
+        Boolean isFollowing = followCommand.type();
-        if (optionalFollowing.isPresent()) { // 이미 팔로우 관계가 존재하는 경우
+        if (optionalFollowing.isPresent()) { // Follow relationship already exists
src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java (1)

16-32: 중첩된 Follower 레코드 구조가 적절합니다.

팔로워 정보를 나타내는 필드들이 완전하고, 정적 팩토리 메서드 of()를 통해 인스턴스 생성이 명확합니다. 다만 followingCountInteger로 선언했는데, 음수가 될 수 없으므로 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1ecc0df and 322b944.

📒 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: 필드명 변경 승인

userIdfollowerUserId로 변경하여 팔로워와 팔로잉 사용자를 명확하게 구분한 것이 좋습니다. 도메인 모델의 의미가 더 명확해졌습니다.


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 컨벤션을 잘 따르고 있습니다:

  1. findByUserAndTargetUser: Optional 반환으로 null 안전성 확보
  2. findFollowersByUserIdBeforeCreatedAt: 커서 기반 페이지네이션을 위한 적절한 파라미터 구성
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java (1)

26-59: 커서 기반 페이지네이션이 잘 구현되었습니다.

N+1 문제를 방지하기 위한 최적화가 적절히 적용되었습니다:

  1. countByFollowingUserIds를 통한 일괄 카운트 조회
  2. 커서 파싱과 페이지네이션 로직이 올바르게 구현됨
  3. UserFollowersResponse 구조가 API 응답에 적합함
src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java (1)

22-145: 포괄적이고 잘 구조화된 단위 테스트입니다.

테스트가 다음과 같은 장점을 가지고 있습니다:

  1. 완전한 시나리오 커버리지: 팔로우/언팔로우의 모든 경우를 테스트
  2. 예외 상황 처리: 자기 자신 팔로우, 존재하지 않는 관계 언팔로우 등
  3. 적절한 검증: ArgumentCaptor를 통한 저장 객체 검증
  4. 명확한 구조: Nested 클래스로 테스트 케이스 그룹화
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java (2)

46-57: 팔로우 관계 조회 메서드가 올바르게 구현되었습니다.

QueryDSL을 사용한 간단하고 명확한 구현입니다. Optional 반환으로 null 안전성을 확보했습니다.


59-81: N+1 문제를 효과적으로 방지한 팔로워 조회 구현입니다.

성능 최적화가 잘 적용되었습니다:

  1. FetchJoin 사용: followerUserJpaEntityaliasForUserJpaEntity에 대한 fetchJoin으로 N+1 문제 방지
  2. 동적 쿼리: BooleanBuilder를 통한 커서 조건 처리
  3. 적절한 필터링: ACTIVE 상태만 조회하는 비즈니스 로직 반영
  4. 정렬과 페이지네이션: 생성일시 내림차순 정렬과 limit 적용
src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java (2)

27-53: 통합 테스트 설정이 적절하게 구성되었습니다.

SpringBootTest와 MockMvc를 사용한 통합 테스트 설정이 올바르며, 테스트 후 데이터 정리도 적절합니다.


55-100: 팔로우/언팔로우 통합 테스트 및 메서드 상속 확인 완료

  • API 응답(isFollowing) 검증
  • DB 상태(ACTIVEINACTIVE) 검증
  • 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;
Copy link

@coderabbitai coderabbitai bot Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

클린 아키텍처 원칙 위반이 발생했습니다.

Port 인터페이스가 adapter.in.web.response 패키지의 UserFollowersResponse를 임포트하고 있습니다. 이는 내부 계층(application)이 외부 계층(adapter)을 참조하는 것으로 클린 아키텍처 원칙에 위반됩니다.

다음 해결 방안을 제안합니다:

  1. application.port.out 패키지에 별도의 응답 모델 생성
  2. 또는 도메인 계층에 응답 모델 정의
  3. 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우리는 Query 즉, 조회 api에 한해서는 response dto가 외부계층에서 참조되는 것을 허용하기로 했어. 이것에 대해서는 어떻게 생각해?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@buzz0331 좋은 접근 방식이라고 생각합니다!

CQRS 아키텍처에서 Query 쪽에 대해 이런 유연한 접근을 취하는 것은 매우 합리적입니다. 몇 가지 이유로 이 결정이 타당하다고 봅니다:

  1. Query는 읽기 전용: Response DTO는 비즈니스 로직을 포함하지 않고 단순히 데이터 표현을 위한 것이므로 의존성 위험이 낮습니다.

  2. 성능 최적화: 불필요한 매핑 계층을 제거하여 조회 성능을 향상시킬 수 있습니다.

  3. CQRS 특성: Command와 Query의 관심사가 다르므로 각각에 적합한 아키텍처 제약을 적용하는 것이 실용적입니다.

  4. 개발 효율성: 조회 전용 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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

클린 아키텍처 원칙 위반이 발생했습니다.

Usecase 인터페이스가 adapter.in.web.response 패키지의 UserFollowersResponse를 임포트하고 있습니다. 이는 application 계층이 adapter 계층을 참조하는 것으로 클린 아키텍처 원칙에 위반됩니다.

다음 해결 방안을 제안합니다:

  1. application.port.in 패키지에 별도의 응답 모델 생성
  2. 또는 도메인 계층에 응답 모델 정의
  3. 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.

Comment on lines 50 to 55
@Test
@DisplayName("type이 null이면 400 에러")
void null_type() throws Exception {
Map<String, Object> req = new HashMap<>(); // type 없음
assertBad(req, "type은 필수 파라미터입니다.");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

hd0rable
hd0rable previously approved these changes Jul 15, 2025
Copy link
Member

@hd0rable hd0rable left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니닷닷👍🏻🤩

private List<UserJpaEntity> followerUsers; // 팔로워 12명

@BeforeEach
void setUp() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@buzz0331 buzz0331 dismissed hd0rable’s stale review July 15, 2025 05:25

The merge-base changed after approval.

@sonarqubecloud
Copy link

@buzz0331 buzz0331 merged commit 6d40c4d into develop Jul 15, 2025
3 checks passed
@buzz0331 buzz0331 deleted the feat/#77-get-follower branch July 15, 2025 05:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[THIP2025-122] [feat] 팔로워 조회 api 개발

2 participants