Skip to content

[feat] 팔로잉 상태 변경 api 개발#76

Merged
buzz0331 merged 20 commits into
developfrom
feat/#69-change-following-state
Jul 15, 2025
Merged

[feat] 팔로잉 상태 변경 api 개발#76
buzz0331 merged 20 commits into
developfrom
feat/#69-change-following-state

Conversation

@buzz0331
Copy link
Copy Markdown
Contributor

@buzz0331 buzz0331 commented Jul 14, 2025

#️⃣ 연관된 이슈

closes #69

📝 작업 내용

팔로잉 상태 변경 api를 개발했습니다.
팔로잉 상태가 변경되는 경우는 다음과 같습니다.

type = true (팔로우 요청)인 경우,
• following(user_id, target_user_id) inactive row가 존재할 경우 → active로 변경
• 존재하지 않는 경우 → 새로운 active row 생성

type = false (언팔로우 요청)인 경우,
• active row가 존재할 경우 → inactive로 변경
• 없는 경우 → 예외 발생

테스트 코드 (서비스 단위테스트 및 Following 도메인 단위테스트에 경우를 나누어 상세히 작성해두었으니 확인 부탁드릴게요!)

📸 스크린샷

스크린샷 2025-07-14 오후 10 37 13

💬 리뷰 요구사항

회의에서 이야기한대로 Follow 테이블에 Soft Delete를 적용했는데 저는 delete 쿼리를 사용하지 않고 도메인 규칙에서 status가 바뀌는 것을 적용시키기 위해서 DB에도 status를 update하는 쿼리를 날리도록 로직을 구성했습니다. 이 점 유의해서 리뷰해주시면 감사하겠습니다 :)

추가적으로 url도 조금더 계층형 구조에 맞게 수정하였는데 이것도 리뷰해주시면 감사할게요~!

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

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

Summary by CodeRabbit

  • New Features

    • 사용자 팔로우/언팔로우 API가 추가되어 다른 사용자를 팔로우하거나 언팔로우할 수 있습니다.
    • 팔로우 상태를 나타내는 응답이 제공됩니다.
    • 팔로우/언팔로우 요청 시 이미 팔로우했거나 언팔로우한 경우, 자기 자신 팔로우 시도 등에 대한 에러 메시지가 반환됩니다.
    • 데이터베이스 레벨에서 팔로우 관계에 대해 소프트 삭제(비활성화) 기능이 도입되었습니다.
  • Bug Fixes

    • 팔로우/언팔로우 상태 전환 시 중복 요청 및 잘못된 상태 변경에 대한 예외 처리가 강화되었습니다.
  • Tests

    • 팔로우/언팔로우 기능과 API에 대한 단위 및 통합 테스트가 추가되었습니다.
    • 누락된 파라미터 및 예외 상황에 대한 검증 테스트가 포함되었습니다.
  • Documentation

    • 일부 테스트 클래스의 설명(디스플레이 네임)이 한글로 개선되었습니다.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jul 14, 2025

"""

Walkthrough

팔로잉 상태를 변경하는 API가 새롭게 도입되었습니다. 도메인, 애플리케이션, 인프라 계층에 팔로우/언팔로우 로직이 구현되었으며, 컨트롤러, 서비스, 포트/어댑터, 예외 및 테스트 코드가 추가 및 확장되었습니다. 팔로잉 상태 변경의 유효성 검증과 예외 처리, 소프트 딜리트 처리도 반영되었습니다.

Changes

파일/그룹 변경 요약
src/main/java/konkuk/thip/common/entity/BaseDomainEntity.java 팔로잉 상태 토글 메서드(changeStatus) 추가
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java 팔로우 관련 에러코드 4종 추가
src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java
src/main/java/konkuk/thip/user/adapter/in/web/request/UserFollowRequest.java
src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowResponse.java
팔로잉 상태 변경 API 엔드포인트, 요청/응답 DTO 추가
src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java 소프트 딜리트(@SQLDelete) 적용
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingJpaRepository.java
FollowingQueryRepository.java
FollowingQueryRepositoryImpl.java
FollowingCommandPersistenceAdapter.java
팔로우 엔티티용 JPA 리포지토리, 쿼리 리포지토리, 구현체, 커맨드 어댑터 신규 추가
src/main/java/konkuk/thip/user/application/port/in/UserFollowUsecase.java
dto/UserFollowCommand.java
팔로우 상태 변경 유스케이스 인터페이스 및 커맨드 DTO 추가
src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java 팔로우 커맨드 포트 인터페이스 추가
src/main/java/konkuk/thip/user/application/service/UserFollowService.java 팔로우 상태 변경 서비스 및 로직 구현
src/main/java/konkuk/thip/user/domain/Following.java 팔로우 상태 생성/변경 메서드 및 유효성 검증, 예외처리 추가
src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java
UserFollowControllerTest.java
팔로우 상태 변경 API 통합/유효성 테스트 추가
src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java 팔로우 서비스 단위 테스트 추가
src/test/java/konkuk/thip/user/domain/FollowingTest.java 팔로우 도메인 상태 변경 단위 테스트 추가
src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java
UserVerifyNicknameControllerTest.java
UserViewAliasChoiceControllerTest.java
DisplayName 한글로 변경 (기능 영향 없음)

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller
    participant Service
    participant PersistenceAdapter
    participant Repository

    Client->>Controller: POST /users/following/{followingUserId} (UserFollowRequest)
    Controller->>Service: changeFollowingState(UserFollowCommand)
    Service->>PersistenceAdapter: findByUserIdAndTargetUserId()
    PersistenceAdapter->>Repository: findByUserAndTargetUser()
    Repository-->>PersistenceAdapter: Optional<FollowingJpaEntity>
    PersistenceAdapter-->>Service: Optional<Following>
    Service->>Service: 유효성 검증 및 상태 변경
    Service->>PersistenceAdapter: save() 또는 updateStatus()
    PersistenceAdapter->>Repository: save() 또는 상태 update
    Repository-->>PersistenceAdapter: 결과 반환
    Service-->>Controller: Boolean (isFollowing)
    Controller-->>Client: BaseResponse<UserFollowResponse>
Loading

Assessment against linked issues

Objective Addressed Explanation
팔로잉 상태 변경 API 구현 (#69)
팔로잉 상태 유효성 검증 및 예외처리 (#69)
팔로우/언팔로우 도메인 및 서비스 로직 구현 (#69)
팔로우 상태 변경 테스트 코드 작성 (#69)

Poem

( ・ㅅ・)ノ
팔로우 토글, 클릭 한 번에
토글토글 친구 맺기
예외도 척척, 검증도 똑똑
토끼도 친구 많아져서
오늘은 더욱 신이 나!
🐇✨
"""


📜 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 b6dcd4b and 799145e.

📒 Files selected for processing (3)
  • src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java (3 hunks)
  • src/main/java/konkuk/thip/user/adapter/in/web/request/UserFollowRequest.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/port/in/dto/UserFollowCommand.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/konkuk/thip/user/application/port/in/dto/UserFollowCommand.java
  • src/main/java/konkuk/thip/user/adapter/in/web/request/UserFollowRequest.java
  • src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java
✨ 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
Copy Markdown

@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: 2

🧹 Nitpick comments (7)
src/main/java/konkuk/thip/user/application/port/in/UserFollowUsecase.java (1)

6-8: 반환 타입 및 예외 처리 방식 검토 필요

현재 Boolean 반환 타입은 단순하지만, PR 설명에 따르면 팔로우/언팔로우 시 다양한 시나리오(활성화, 비활성화, 예외 상황)를 처리해야 합니다. 예외 상황(예: 이미 언팔로우된 상태에서 언팔로우 시도)에 대한 처리가 Boolean 반환값으로는 충분하지 않을 수 있습니다.

다음과 같은 방식들을 고려해보세요:

  1. void 반환 타입 + 예외 처리
  2. Result 패턴 사용
  3. 상태 정보를 포함한 응답 객체 반환
src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java (1)

7-7: @SQLDelete에서 StatusType enum 상수 사용을 권장

소프트 삭제 구현에서 하드코딩된 문자열 대신 StatusType enum을 활용하는 것이 좋습니다. 하지만 어노테이션의 특성상 컴파일 타임 상수만 사용 가능하므로, 현재 구현이 불가피할 수 있습니다.

다음과 같이 StatusType에 상수를 정의하여 사용하는 것을 고려해보세요:

-@SQLDelete(sql = "UPDATE followings SET status = 'INACTIVE' WHERE following_id = ?")
+@SQLDelete(sql = "UPDATE followings SET status = '" + StatusType.INACTIVE_VALUE + "' WHERE following_id = ?")

단, StatusType.INACTIVE_VALUE가 컴파일 타임 상수로 정의되어야 합니다.

Also applies to: 12-12

src/main/java/konkuk/thip/user/adapter/in/web/request/UserFollowRequest.java (1)

7-7: 필드명 개선 제안

'type' 필드명이 다소 일반적입니다. 더 명확한 의미 전달을 위해 'isFollowRequest' 또는 'followAction' 등을 고려해보세요.

-        Boolean type // true -> 팔로우, false -> 언팔로우
+        Boolean isFollowRequest // true -> 팔로우, false -> 언팔로우
src/main/java/konkuk/thip/user/application/port/in/dto/UserFollowCommand.java (1)

8-10: 정적 팩토리 메서드명 개선 제안

'toCommand' 보다는 'of' 또는 'from' 같은 일반적인 네이밍 컨벤션을 사용하는 것이 좋습니다.

-    public static UserFollowCommand toCommand(Long userId, Long targetUserId, Boolean type) {
+    public static UserFollowCommand of(Long userId, Long targetUserId, Boolean type) {
         return new UserFollowCommand(userId, targetUserId, type);
     }
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingJpaRepository.java (1)

7-8: @repository 어노테이션이 불필요합니다.

Spring Data JPA에서 JpaRepository를 상속받는 인터페이스는 자동으로 Spring 컨테이너에 빈으로 등록되므로 @Repository 어노테이션이 필요하지 않습니다.

-@Repository
 public interface FollowingJpaRepository extends JpaRepository<FollowingJpaEntity, Long>, FollowingQueryRepository {
src/test/java/konkuk/thip/user/adapter/in/web/UserFollowControllerTest.java (1)

34-38: 테스트 커버리지를 확장하는 것을 권장합니다.

현재 type 파라미터가 null인 경우만 테스트하고 있습니다. 완전한 검증을 위해 다음 케이스들도 추가 테스트하는 것이 좋습니다:

  • type 파라미터가 잘못된 타입인 경우
  • 정상적인 요청의 경우
src/main/java/konkuk/thip/user/domain/Following.java (1)

30-36: 상태 변경 메서드의 반환값이 불필요합니다.

메서드 로직은 올바르게 구현되었지만, isFollowRequest 값을 그대로 반환하는 것은 의미가 없어 보입니다. 호출자가 이미 이 값을 알고 있기 때문입니다.

반환 타입을 void로 변경하는 것을 고려해보세요:

-    public boolean changeFollowingState(boolean isFollowRequest) {
+    public void changeFollowingState(boolean isFollowRequest) {
         StatusType currentStatus = getStatus();
         validateFollowingState(isFollowRequest, currentStatus);

         super.changeStatus();
-        return isFollowRequest;
     }
📜 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 57edf6b.

📒 Files selected for processing (22)
  • 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/user/adapter/in/web/UserCommandController.java (3 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/out/jpa/FollowingJpaEntity.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/FollowingQueryRepository.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/port/in/UserFollowUsecase.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/service/UserFollowService.java (1 hunks)
  • src/main/java/konkuk/thip/user/domain/Following.java (2 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/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 (5)
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/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/FollowingQueryRepositoryImpl.java (1)
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/FollowingQueryRepository.java (1)
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로부터 조회하는 메서드를 추가함.
🧬 Code Graph Analysis (4)
src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java (1)
src/main/java/konkuk/thip/user/domain/Following.java (1)
  • Getter (12-47)
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingJpaRepository.java (1)
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java (1)
  • Repository (11-29)
src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (14-138)
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java (1)
src/main/java/konkuk/thip/common/exception/EntityNotFoundException.java (1)
  • EntityNotFoundException (5-10)
🔇 Additional comments (30)
src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java (1)

33-33: 테스트 클래스 설명 개선

기존 클래스명 기반 설명에서 API 기능을 명확히 표현하는 설명으로 개선되었습니다.

src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java (1)

34-34: 테스트 클래스 설명 개선

기존 클래스명 기반 설명에서 API 기능을 명확히 표현하는 설명으로 개선되었습니다.

src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java (1)

33-33: 테스트 클래스 설명 개선

기존 클래스명 기반 설명에서 API 기능을 명확히 표현하는 설명으로 개선되었습니다.

src/main/java/konkuk/thip/common/entity/BaseDomainEntity.java (1)

18-24: 소프트 삭제 패턴 구현

팔로우/언팔로우 기능을 위한 상태 토글 메서드가 올바르게 구현되었습니다. ACTIVE/INACTIVE 상태 전환을 통해 소프트 삭제 방식을 지원합니다.

src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java (1)

12-12: SQLDelete 매핑 검증 완료

FollowingJpaEntity의 @SQLDelete 구문은 다음과 같이 올바르게 설정되어 있습니다.

  • following_id 컬럼은 @Column(name = "following_id")로 매핑된 followingId 필드와 일치
  • status 필드는 BaseJpaEntity에서 기본 컬럼명(status)으로 매핑되어 있어
    SET status = 'INACTIVE' 구문에도 문제가 없음
  • 파라미터 바인딩용 ? 플레이스홀더는 단일 @id 필드에 대해 정상 작동

따라서 추가 변경 없이 이대로 유지하셔도 됩니다.

src/main/java/konkuk/thip/user/adapter/in/web/request/UserFollowRequest.java (1)

1-11: Record와 validation 어노테이션 사용이 적절함

Request DTO 구현이 잘 되어 있습니다. Record 사용과 @NotNull 검증이 적절하며, Boolean 래퍼 타입 사용으로 null 체크가 가능합니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepository.java (1)

1-10: 우수한 쿼리 인터페이스 설계

Optional 반환 타입 사용으로 null 안전성을 보장하고, 메서드명이 명확하게 기능을 표현합니다. 단일 책임 원칙을 잘 따르고 있습니다.

src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowResponse.java (2)

1-10: 깔끔한 응답 DTO 구현

Record 사용과 정적 팩토리 메서드 패턴이 적절하게 적용되었습니다. 필드명 'isFollowing'이 명확하고, primitive boolean 사용이 응답 객체에 적합합니다.


6-8: 정적 팩토리 메서드 패턴 우수

of 메서드를 통한 인스턴스 생성이 코드 가독성을 향상시킵니다. 생성자 직접 호출보다 의미가 명확합니다.

src/main/java/konkuk/thip/user/application/port/in/dto/UserFollowCommand.java (1)

1-12: Command 패턴 구현이 적절함

CQRS 패턴을 따른 Command 객체 설계가 잘 되어 있습니다. Record 사용과 불변성 보장이 적절합니다.

src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java (1)

7-14: CQRS 패턴을 올바르게 구현했습니다.

CommandPort에 findByUserIdAndTargetUserId 메서드를 포함하여 도메인 엔티티를 조회하는 패턴을 정확히 따르고 있습니다. 메서드 시그니처도 적절하게 정의되어 있습니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java (1)

18-28: QueryDSL 구현이 올바르게 작성되었습니다.

사용자 ID와 타겟 사용자 ID를 통한 팔로우 관계 조회 로직이 정확하게 구현되어 있습니다. Optional.ofNullable() 사용으로 null 안전성도 확보되었습니다.

src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)

40-47: 팔로우 기능 에러 코드가 적절하게 추가되었습니다.

팔로우/언팔로우 기능에 필요한 에러 코드들이 논리적으로 잘 정의되어 있습니다. HTTP 상태 코드와 메시지도 적절합니다.

src/test/java/konkuk/thip/user/adapter/in/web/UserFollowControllerTest.java (1)

40-48: 테스트 헬퍼 메서드가 잘 구현되었습니다.

assertBad 메서드를 통해 에러 케이스 검증 로직을 재사용 가능하게 만든 것이 좋습니다. 에러 코드와 메시지 검증도 정확하게 수행하고 있습니다.

src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java (1)

54-62: 팔로우 API 엔드포인트 구현이 깔끔합니다.

REST 설계 원칙을 잘 따르고 있고, 입력 검증과 응답 구조가 일관성 있게 구현되었습니다. 한국어 주석도 기능을 명확히 설명하고 있습니다.

src/test/java/konkuk/thip/user/domain/FollowingTest.java (3)

16-48: 팔로우 상태 변경 로직이 체계적으로 테스트되었습니다.

중첩 클래스를 사용한 테스트 구조가 깔끔하고, 정상 케이스와 예외 케이스를 모두 적절히 검증하고 있습니다.


50-82: 언팔로우 테스트도 완벽하게 구현되었습니다.

상태 변경과 예외 처리 로직이 정확히 검증되고 있습니다.


84-92: 새로운 팔로우 생성 테스트가 적절합니다.

기본 상태가 ACTIVE로 설정되는 것을 확인하는 테스트가 잘 구현되어 있습니다.

src/main/java/konkuk/thip/user/application/service/UserFollowService.java (2)

24-47: 팔로우 상태 변경 로직이 올바르게 구현되었습니다.

기존 관계 존재 여부에 따른 분기 처리와 예외 처리가 적절하며, 트랜잭션 처리도 잘 적용되어 있습니다.


49-53: 자기 자신 팔로우 검증이 적절합니다.

Objects.equals를 사용한 null-safe 비교가 좋습니다.

src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java (1)

55-100: 통합 테스트가 전체 플로우를 완벽하게 검증합니다.

API 호출부터 데이터베이스 상태 변경까지 전체 플로우를 테스트하고 있으며, 팔로우 후 언팔로우 시나리오가 잘 구현되어 있습니다. 상태 변경도 정확히 검증하고 있습니다.

src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java (3)

33-86: 팔로우 시나리오 테스트가 완벽합니다.

기존 비활성 관계를 활성화하는 케이스와 새로운 관계를 생성하는 케이스를 모두 적절히 테스트하고 있습니다. ArgumentCaptor를 사용한 검증도 좋습니다.


88-133: 언팔로우 시나리오와 예외 처리 테스트가 우수합니다.

활성 관계를 비활성화하는 케이스와 관계가 없는 상태에서 언팔로우 시 예외 처리가 잘 테스트되어 있습니다.


135-144: 자기 자신 팔로우 검증 테스트가 적절합니다.

비즈니스 규칙을 정확히 검증하는 테스트입니다.

src/main/java/konkuk/thip/user/domain/Following.java (2)

22-28: static factory method 구현이 적절합니다.

새로운 Following 인스턴스를 생성하는 static factory method가 잘 구현되어 있습니다. 초기 상태를 ACTIVE로 설정하는 것이 팔로우 요청의 기본 동작과 일치합니다.


38-46: 유효성 검증 로직이 명확하고 적절합니다.

상태 전환의 유효성을 검증하는 로직이 잘 구현되어 있습니다. 도메인 규칙에 맞는 적절한 예외를 던지고 있으며, 주석도 이해를 돕습니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java (4)

21-24: 의존성 주입이 적절하게 구성되었습니다.

JPA 리포지토리와 매퍼의 의존성이 올바르게 주입되어 있습니다. CQRS 패턴에 따라 CommandPort 구현체로서 적절한 구조를 갖추고 있습니다.


26-30: 조회 메서드 구현이 적절합니다.

Optional을 활용한 안전한 조회와 매퍼를 통한 도메인 엔티티 변환이 올바르게 구현되어 있습니다.


32-41: 유저 존재 여부 검증과 예외 처리가 적절합니다.

팔로우 관계를 저장하기 전에 두 유저 모두의 존재 여부를 확인하고 적절한 예외를 던지는 로직이 잘 구현되어 있습니다. 도메인 규칙을 충실히 따르고 있습니다.


43-49: 트랜잭션 경계 검증 완료: UserFollowService에 @transactional 적용됨

서비스 계층인 UserFollowService 클래스(파일: src/main/java/konkuk/thip/user/application/service/UserFollowService.java 25행)에 이미 @Transactional 어노테이션이 선언되어 있어, FollowingCommandPersistenceAdapter.updateStatus() 호출이 트랜잭션 범위 내에서 안전하게 실행됩니다. 추가 조치가 필요 없습니다.

  • UserFollowService.java(25행): @Transactional 확인 완료


import jakarta.persistence.*;
import konkuk.thip.common.entity.BaseJpaEntity;
import konkuk.thip.common.entity.StatusType;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

StatusType import가 추가되었으나 @SQLDelete에서 활용되지 않음

StatusType enum을 import했지만, @SQLDelete 어노테이션에서는 여전히 하드코딩된 문자열 'INACTIVE'를 사용하고 있습니다.

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java at
line 5, the StatusType enum is imported but not used in the @SQLDelete
annotation, which still uses the hardcoded string 'INACTIVE'. Replace the
hardcoded 'INACTIVE' string in the @SQLDelete annotation with a reference to the
appropriate StatusType enum value to ensure consistency and avoid hardcoding.

Comment thread src/main/java/konkuk/thip/common/exception/code/ErrorCode.java Outdated
Copy link
Copy Markdown

@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: 1

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 57edf6b and 00c49ea.

📒 Files selected for processing (3)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1 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)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
🧰 Additional context used
🧠 Learnings (1)
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepository.java (2)
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 안전성을 위해 권장됩니다.
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로부터 조회하는 메서드를 추가함.
⏰ 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 (1)
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepository.java (1)

3-5: 추가된 import 문들이 적절합니다.

새로운 메서드에 필요한 FollowingJpaEntityOptional import가 정확히 추가되었습니다.


public interface FollowingQueryRepository {
Map<Long, Integer> countByFollowingUserIds(List<Long> userIds);
Optional<FollowingJpaEntity> findByUserAndTargetUser(Long userId, Long targetUserId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

CQRS 분리 원칙에 대한 검토가 필요합니다.

메서드명과 Optional 반환 타입 사용은 적절하지만, CQRS 분리 관점에서 확인이 필요합니다. 학습된 컨벤션에 따르면 "CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가"해야 하는데, 이 메서드는 QueryRepository에 위치하고 있습니다.

이 메서드가 실제로 command 연산(팔로우/언팔로우 상태 변경)에 사용된다면 CommandPort로 이동하는 것이 CQRS 원칙에 더 부합할 수 있습니다.

다음 스크립트로 이 메서드의 사용 패턴을 확인해보세요:


🏁 Script executed:

#!/bin/bash
# 이 메서드가 command 연산에 사용되는지 확인
rg -A 5 -B 5 "findByUserAndTargetUser" --type java

Length of output: 7749


🏁 Script executed:

#!/bin/bash
# Display the FollowingJpaRepository to see its declarations
rg -A 5 -B 5 "interface FollowingJpaRepository" -n src/main/java/konkuk/thip/user/adapter/out/persistence

Length of output: 1179


🏁 Script executed:

#!/bin/bash
# 포트 인터페이스 확인
rg -A 5 -B 5 "interface FollowingCommandPort" -n src/main/java/konkuk/thip
rg -A 5 -B 5 "interface FollowingQueryPort" -n src/main/java/konkuk/thip

Length of output: 2175


CQRS 포트 분리: Command용 findByUserAndTargetUser 메서드 위치 조정 필요

현재 findByUserAndTargetUser가 FollowingQueryRepository에 선언되어 있지만, 이 메서드는 Command 연산(조회 후 상태 변경·저장)에만 사용됩니다. 학습된 컨벤션에 따라 다음과 같이 수정해주세요:

  • src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepository.java에서
    - Optional<FollowingJpaEntity> findByUserAndTargetUser(Long userId, Long targetUserId);
    제거
  • src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingJpaRepository.java
    Optional<FollowingJpaEntity> findByUserAndTargetUser(Long userId, Long targetUserId);
    선언 추가 (Spring Data JPA가 자동 구현)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java에서
    해당 메서드 구현(findByUserAndTargetUser 오버라이드) 삭제

위 변경으로
• CommandPort(및 CommandPersistenceAdapter)만 Command용 조회 메서드를 갖게 되고
• QueryPort에는 오직 조회 API 응답용 countByFollowingUserIds만 남습니다.

🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepository.java
at line 11, remove the method declaration of findByUserAndTargetUser as it is
used only for command operations. Then, add this method declaration to
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingJpaRepository.java
to let Spring Data JPA implement it automatically. Finally, delete the
implementation of findByUserAndTargetUser from
src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryRepositoryImpl.java
to keep query and command responsibilities separated.

Copy link
Copy Markdown
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.

테스트코드 세세하게 짜주셔서 코드이해하는데 술술 잘 읽힌것같아요!! 사소한 리뷰 하나 남겼는데 확인 부탁드립니당당

@Entity
@Table(name = "followings")
@Getter
@SQLDelete(sql = "UPDATE followings SET status = 'INACTIVE' WHERE following_id = ?")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍🏻

Long targetUserId,
Boolean type // true -> 팔로우, false -> 언팔로우
) {
public static UserFollowCommand toCommand(Long userId, Long targetUserId, Boolean type) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

p2: Request → Command 변환은 Adapter 계층인(Request DTO)에서 하는 것이 어떨까요??
Application 계층인 Command가 외부 입력에 대한 의존성이 없어야 하기 때문에,
Request에서 변환해서 넘기는 것이 더 나을 것같습니다!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

넵 수정하겠습니다 👍🏻

@sonarqubecloud
Copy link
Copy Markdown

@buzz0331 buzz0331 merged commit a410854 into develop Jul 15, 2025
3 checks passed
@buzz0331 buzz0331 deleted the feat/#69-change-following-state 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-117] [feat] 팔로잉 상태 변경

2 participants