Skip to content

Conversation

@buzz0331
Copy link
Contributor

@buzz0331 buzz0331 commented Nov 24, 2025

#️⃣ 연관된 이슈

closes #

📝 작업 내용

첫번째 해결책으로는 User 조회시에 비관락을 걸어서 해결해보려고 하였습니다.
현재 팔로잉 코드 흐름이

  1. 팔로잉 테이블에서 팔로잉 관계 조회 FollowingCommandPort.findByUserIdAndTargetUserId()
  2. 타겟 유저(팔로잉 당하는 유저) 조회 UserCommandPort.findById()
  3. save 요청 followingCommandPort.save()
    1. 액션 유저(팔로잉 하는 유저) 조회
    2. 팔로잉 관계 테이블에 행 삽입 → User의 S-Lock 획득
    3. 타겟 유저 조회 후 업데이트 → User의 X-Lock 획득

다음과 같을때, 2번에서 User의 X-Lock을 먼저 획득하면 뒤에서 S-Lock -> X-Lock으로의 승격을 기다리지 않아 데드락을 해결할 수 있을 거라고 추론하였습니다.
부하테스트 결과, 데드락이 발생하지 않았습니다. 다만, 점진적으로 부하를 올리면서 측정해본 결과 VU = 5000에서 최대 20초정도의 요청 지연이 발생했고, VU = 10000에서는 무수히 많은 request timeout과 최대 1분까지의 요청 지연이 발생하는 것을 볼 수 있었습니다.

우선, 이 과정을 통해 비관락을 걸었을 때 데이터 정합성이 지켜지는 것을 보장하긴 하지만, 요청 지연이 너무 발생하는 것과 VU = 10000이 넘어가는 순간 요청 실패율이 급격하게 올라가는 것을 확인했습니다.

따라서, 이후 해결과정에서는 이벤트 기반 비동기 집계를 도입하여 요청 지연을 줄이고 조금더 대용량 트래픽을 견딜 수 있는 해결법을 강구해볼 생각입니다.

정리한 노션입니다. 참고해주세요.
https://separate-snowplow-d8b.notion.site/API-2b2b701eb0b880ae9cfbdff2ba1f5d2f?source=copy_link

📸 스크린샷

💬 리뷰 요구사항

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

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

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

Summary by CodeRabbit

  • 새로운 기능

    • 팔로우 상태 변경에 자동 재시도(Retry) 및 복구 경로 추가
    • k6 기반 팔로우 관련 부하/토글 테스트 스크립트 추가
  • 개선사항

    • 팔로우 중복 방지를 위한 DB 유니크 제약 도입
    • 대상 사용자 조회 시 행 잠금(락) 적용으로 동시성 안전성 강화
    • 자원 잠금 상황을 나타내는 오류 코드 추가
    • Spring Retry 의존성 추가
  • 테스트

    • 대규모 동시성 시나리오를 검증하는 통합 및 부하 테스트 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 24, 2025

Walkthrough

스프링 Retry와 비관적 잠금, 팔로잉 복합 고유 제약을 도입하고, 관련 포트/리포지토리/퍼시스턴스/서비스에 잠금 기반 조회와 재시도/복구 로직을 추가하며, k6 부하 테스트 스크립트와 동시성 통합 테스트, DB 마이그레이션을 포함합니다.

Changes

Cohort / File(s) Summary
의존성
\build.gradle``
implementation 'org.springframework.retry:spring-retry' 의존성 추가
Spring 설정
\src/main/java/konkuk/thip/config/RetryConfig.java``
@Configuration@EnableRetry(proxyTargetClass = true) 설정 클래스 추가
에러 코드
\src/main/java/konkuk/thip/common/exception/code/ErrorCode.java``
RESOURCE_LOCKED(HttpStatus.LOCKED, 50200, "자원이 잠겨 있어 요청을 처리할 수 없습니다.") enum 상수 추가
JPA 엔티티 및 마이그레이션
\src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java`<br>`src/main/resources/db/migration/V251120__Add_following_unique_constraint.sql``
followings 테이블에 (user_id, following_user_id) 복합 고유 제약(uq_followings_user_target) 추가
리포지토리
\src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java``
findByUserIdWithLock(Long userId) 메서드 추가 (pessimistic write lock, timeout 5s)
퍼시스턴스 어댑터
\src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java``
잠금 기반 조회를 수행하는 findByIdWithLock(Long userId) 메서드 추가
포트 인터페이스
\src/main/java/konkuk/thip/user/application/port/out/UserCommandPort.java``
findByIdWithLock(Long userId) 메서드 시그니처 추가
비즈니스 로직
\src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java``
@Retryable(최대 3회, 백오프) 및 @Recover 복구 메서드 추가; 대상 사용자 조회를 findByIdWithLock로 변경; 실패 시 RESOURCE_LOCKED로 복구 처리
부하 테스트
\loadtest/follow_change_state_load_test.js`<br>`loadtest/follow_change_toggle_load_test.js``
k6 기반 팔로우 상태 변경/토글 시나리오 스크립트 추가 (토큰 프리패치, 메트릭, 에러 분류 포함)
동시성 테스트
\src/test/java/konkuk/thip/user/concurrency/UserFollowConcurrencyTest.java``
다중 스레드 동시 팔로우 요청 통합 테스트 추가 (MockMvc, JDBC 확인, Barrier 동기화)
단위/통합 테스트 조정
\src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java``
테스트 내 타겟 사용자 조회를 findByIdWithLock 호출로 변경

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client as 클라이언트
    participant Controller as 컨트롤러
    participant Service as UserFollowService
    participant RetryAspect as Spring-Retry
    participant Repo as UserJpaRepository
    participant DB as Database

    Client->>Controller: POST /users/following/{targetId}
    Controller->>Service: changeFollowingState(command)

    activate RetryAspect
    Note over RetryAspect: 최대 3회 시도, 백오프 정책 적용

    RetryAspect->>Service: invoke
    Service->>Repo: findByUserIdWithLock(targetId) (PESSIMISTIC_WRITE, timeout)
    Repo->>DB: SELECT ... FOR UPDATE
    DB-->>Repo: row + lock 또는 LockTimeout

    alt Lock 획득
        Repo-->>Service: UserJpaEntity
        Service->>DB: 상태 변경 및 저장 (follow/unfollow)
        DB-->>Service: 성공
        RetryAspect-->>Controller: 응답 200/적절한 코드
    else Lock 실패 / 예외
        DB-->>Repo: LockException
        Repo-->>Service: 예외 전파
        RetryAspect->>Service: 재시도 (최대 3회)
        alt 모든 재시도 실패
            Service->>Service: recoverChangeFollowingState()
            Service-->>Controller: BusinessException(RESOURCE_LOCKED)
        end
    end

    deactivate RetryAspect
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • 집중 검토 대상:
    • src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java (재시도/복구 흐름, 예외 매핑)
    • src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java (락 옵션·타임아웃·쿼리)
    • DB 마이그레이션 파일(V251120...) 및 기존 데이터와의 충돌 가능성
    • loadtest 스크립트와 동시성 테스트의 신뢰성/동기화 로직

Possibly related PRs

Suggested reviewers

  • hd0rable
  • seongjunnoh

Poem

🐰 내가 깡충깡충 와서 말하지,
락 하나 걸고 재시도 셋, 천천히 흘러가네.
제약 하나로 중복은 막고,
부하 아래서도 조심스레 뛰어,
안정된 팔로우의 당근 한 조각 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 팔로잉 동시성 이슈 해결을 명확히 다루고 있으며, 변경 사항들(락 메커니즘, 재시도 설정, 부하 테스트)이 모두 이 목표를 지원합니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/#336-follow-count-1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 0

🧹 Nitpick comments (10)
loadtest/follow_change_toggle_load_test.js (1)

5-27: k6 토글 시나리오/메트릭 구성이 명확하고 실험 목적에 잘 맞습니다 (소소한 정리 제안)

  • 토큰 배치 발급 + per-vu-iterations + START_DELAY_S 조합으로 동시 토글 부하를 재현하는 구조가 명확하고, 토큰 발급 실패시 빈 토큰을 넣고 VU 쪽에서 !token이면 조용히 스킵하는 전략도 실용적인 선택으로 보입니다.
  • 전역 isFollowing를 VU별 토글 상태로 사용하는 것도 k6의 VU 격리 특성 상 의도대로 동작할 것이므로 유지해도 괜찮아 보입니다.
  • 자잘한 부분으로는, setup 마지막의 if (tokens.length > USERS_COUNT) tokens.length = USERS_COUNT;는 현재 로직 상 항상 길이가 정확히 USERS_COUNT가 되므로 삭제해도 동작에는 영향이 없습니다.
  • 또 하나는 ERR 상수와 서버 쪽 에러 코드가 반드시 함께 유지되어야 하니, 상수 선언부에 “서버 ErrorCode 변경 시 함께 수정 필요” 정도의 주석을 추가해 두면 나중에 코드 동기화가 조금 더 쉬울 것 같습니다.

Also applies to: 35-47, 49-64, 66-93, 95-162

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

8-16: CommandPort에 Lock 버전 조회 메서드 추가는 설계상 자연스럽습니다 (간단한 Javadoc 추천)

  • findById 옆에 findByIdWithLock를 두고, follow 쓰기 플로우에서만 사용하는 구조는 CommandPort에 도메인 엔티티 조회 메서드를 두는 기존 CQRS 컨벤션과 잘 맞습니다.
  • 다만 이 메서드가 “비관적 write 락 + 타임아웃” 전제를 가지는 만큼, 인터페이스 수준에 간단히 Javadoc으로 락 타입·타임아웃·예외 동작을 명시해 두면 다른 Adapter/UseCase에서 오용될 여지를 줄일 수 있을 것 같습니다. (기존 CQRS Port 분리 컨벤션 learnings 기준입니다.)
src/main/resources/db/migration/V251120__Add_following_unique_constraint.sql (1)

1-3: 팔로잉 유니크 제약 추가는 타당하며, 운영 DB의 중복 레코드 여부만 한 번 점검해 두면 좋겠습니다

  • (user_id, following_user_id)에 유니크 제약을 거는 방향은 follow 관계를 1회로 제한하고, 이후 동시성 이슈에 대한 방어선으로도 적절해 보입니다.

  • 다만 운영 DB에 이미 중복 레코드가 있다면 이 마이그레이션이 바로 실패할 수 있으니, 적용 전에 예를 들어 아래와 같은 쿼리로 중복 여부를 한 번 확인하고 필요 시 정리하는 절차를 추천드립니다.

    SELECT user_id, following_user_id, COUNT(*) AS cnt
    FROM followings
    GROUP BY user_id, following_user_id
    HAVING COUNT(*) > 1;
  • 이 제약은 동시에 유니크 인덱스를 생성하므로, (user_id, following_user_id) 기준 조회/삽입 성능에도 도움이 될 것입니다.

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

3-4: PESSIMISTIC_WRITE 기반 findByUserIdWithLock 추가가 의도에 잘 맞습니다 (타임아웃·트랜잭션 사용 컨벤션만 정리 권장)

  • findByUserId를 건드리지 않고, 별도 메서드에 @Lock(PESSIMISTIC_WRITE)와 5초 lock.timeout 힌트를 부여한 구조는 “동시성 민감한 write 플로우 전용 조회”라는 의도가 잘 드러나고, 이전에 정리한 findByUserId 선호 컨벤션과도 일관적입니다. (UserJpaRepository 관련 learnings 기준입니다.)
  • 한편 이 메서드는 락 대기만 최대 5초까지 걸릴 수 있고, 상위 레이어에서 @Retryable까지 적용된 상태라면 재시도 횟수에 따라 최악 응답 시간이 꽤 길어질 수 있습니다. 현재 Retry 설정(시도 횟수, backoff)과 함께 전체 upper bound를 한 번 계산해 보고, 필요한 경우 락 타임아웃이나 retry 정책을 조금 더 보수적으로 조정하는 것도 고려해 볼 만합니다.
  • 또한 이 메서드는 반드시 @Transactional(readOnly = false) 문맥 안에서만 호출되어야 하니, follow 서비스 레벨에서 “락 기반 조회는 write 트랜잭션에서만 사용” 정도의 팀 컨벤션을 명시해 두면 이후 유지보수 시 안전할 것 같습니다.

Also applies to: 7-10, 22-27

src/test/java/konkuk/thip/user/concurrency/UserFollowConcurrencyTest.java (2)

36-120: 동시성 테스트의 검증 조건이 지나치게 느슨해 회귀를 잘 못 잡을 수 있습니다.

지금은 followings 행 수와 follower_count 가 단순히 followerCount 이하인지만 검사해서, 요청이 거의 다 실패하거나 0건이어도 테스트가 통과할 수 있습니다. 최소한 okCount 와 DB 값 간의 관계(예: followingRowsstoredFollowerCount, 또는 okCount 와의 기대 관계 등)를 한두 개 정도 추가로 assert 해 두면 실제 정합성 문제가 다시 생겼을 때 더 일찍 감지할 수 있을 것 같습니다.


123-129: 테스트 유저 생성 시 oauth2Id 를 고유하게 만들어 두면 더 안전합니다.

createUsersRange 에서 TestEntityFactory.createUser 를 반복 호출할 때, 팩토리 내부가 고정된 oauth2Id 를 사용한다면 해당 컬럼에 유니크 제약을 추가하는 순간 이 테스트가 제약 위반으로 깨질 수 있습니다. \"kakao_12345678_\" + i 와 같이 follower 별로 다른 oauth2Id 를 부여하도록 팩토리를 확장하거나, 여기서만 별도 빌더를 사용해 고유한 값을 주는 방향을 고려해 보시면 좋겠습니다.

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

35-46: @Retryable / @Recover 예외 분류는 의도가 잘 드러나지만, 범위/로깅을 약간 좁히는 걸 추천드립니다.

  • notRecoverable + noRetryForBusinessException, InvalidStateException 을 넣어 도메인 예외는 재시도/복구를 완전히 건너뛰는 구조는 좋아 보입니다.
  • 다만 retryFor 를 지정하지 않아, 도메인 예외를 제외한 모든 예외(예: 잠금/DB 예외뿐 아니라 NPE, 버그성 RuntimeException 등)까지 3회 재시도 후 RESOURCE_LOCKED 로 매핑됩니다. 이 경우 실제 버그가 “락 문제”로 보일 수 있어 원인 파악이 어려워질 수 있습니다.
  • 가능하다면:
    • 잠금/DB 관련 예외(예: JPA 락 타임아웃 계열)만 retryFor 로 한정하거나,
    • 최소한 @Recover 안에서 efollowCommand를 로그로 남기거나(또는 BusinessException에 cause 를 연결) 해두면, 운영 중 디버깅이 훨씬 수월해질 것 같습니다.

1차 대응으로는 충분히 간단하고 명확한 설계라 유지하셔도 되지만, 다음 튜닝 단계에서 한 번 정도는 재시도 대상/복구 대상 예외 범위를 정리해 보면 좋겠습니다. 과도한 복잡도는 피하고자 하는 선호를 고려해 권장사항 수준으로만 제안드립니다. Based on learnings, ...

Also applies to: 73-76


55-55: findByIdWithLock 적용으로 타깃 유저에 대한 선점 락을 거는 방향은 타당해 보입니다만, 다른 경로와의 락 순서 일관성은 한 번 더 확인해 주세요.

  • userCommandPort.findByIdWithLock(targetUserId) 로 X-lock(또는 P-lock)을 먼저 잡도록 바꾼 건 데드락 재현 케이스를 줄이는 데 도움이 될 것 같습니다.
  • 다만 이 메서드 밖의 다른 플로우(예: 언팔로우 이외의 팔로우 관련 유스케이스, 배치/정합성 보정 로직 등)에서도 같은 테이블에 접근할 때 락 획득 순서가 동일한지 확인해 두지 않으면, 새로운 데드락 패턴이 생길 여지가 있습니다.
  • 성능 테스트에서 관찰하신 대로 고부하 환경에서는 여기서 대기열이 길어질 수 있으니, 운영에 반영할 때는 모니터링 지표(락 대기 시간, DB wait event 등)를 한 번 더 보면서 튜닝 포인트로 잡아두시면 좋겠습니다.
loadtest/follow_change_state_load_test.js (2)

24-28: 4xx 허용 범위와 에러 코드 매핑을 조금 더 좁히면 장애 감지가 쉬워질 것 같습니다.

  • ERR.* 상수로 “이미 팔로우/언팔로”, “자기 자신 팔로우”를 구분하고, 4xx 일 때 각각 카운터를 나눠 쌓는 구조는 매우 좋습니다.
  • 다만 check 에서
    'follow_change 200 or expected 4xx': (r) => r.status === 200 || (r.status >= 400 && r.status < 500),
    로 모든 4xx 를 “expected” 로 허용하고 있어, 인증 실패(401), 권한 오류(403), 라우팅 오류(404) 등도 체크상으로는 통과하게 됩니다.
  • 부하 테스트가 “락/중복 팔로우에 대한 4xx 는 허용하되, 그 외 4xx 는 빨리 드러내고 싶다”는 목적이라면:
    • fail_OTHER_4XX 에 대해 threshold: ['count==0'] 를 추가하거나,
    • check 식을 status === 200 || (status >= 400 && status < 500 && [ERR.USER_ALREADY_FOLLOWED, ...].includes(err.code)) 형태로 좁히는 방안을 고려해 볼 수 있습니다. (이 경우 parseError 재호출 비용과 가독성을 감안해 선택)
  • 지금도 메트릭을 보면 구분은 가능하지만, CI 등 자동 판단에는 위와 같이 한 번 더 조건을 좁혀 두는 편이 안정적일 것 같습니다.

Also applies to: 123-138, 146-148


63-93: 토큰 발급 실패 시 VU 수가 줄어드는 현상은 의도인지 한 번만 더 확인해 보시면 좋겠습니다.

  • setup() 에서 토큰을 배치로 발급하고, 실패한 경우에도 인덱스를 맞추기 위해 '' 를 push 한 뒤 token_issue_failed 를 올리는 구조는 이해하기 쉽습니다.
  • 이후 default 함수에서:
    const token = data.tokens[idx];
    ...
    if (!token) {
        return;
    }
    으로 토큰이 비어 있으면 해당 VU 는 팔로우 요청을 아예 보내지 않고 종료합니다. 토큰 발급 실패가 많아지면, 실제 동시 팔로우 요청 수가 USERS_COUNT 보다 적어질 수 있습니다.
  • 만약 “토큰 발급 실패가 1건이라도 있으면 테스트 자체를 실패로 보겠다”는 의도라면:
    • options.thresholdstoken_issue_failed: ['count==0'] 를 추가하거나,
    • setup() 내에서 token_issue_failed 가 0이 아니면 fail 을 던져 테스트를 중단하는 방법도 있습니다.
  • 반대로 “일부 토큰 실패는 감수하고, 가능한 만큼만 부하를 걸겠다”는 의도라면 지금 구조도 충분히 실용적이니, 주석으로 그 의도를 한 줄 남겨 두면 나중에 스크립트를 보는 사람이 이해하기 더 쉬울 것 같습니다.

Also applies to: 105-107

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b7ecd53 and 0031c52.

📒 Files selected for processing (12)
  • build.gradle (1 hunks)
  • loadtest/follow_change_state_load_test.js (1 hunks)
  • loadtest/follow_change_toggle_load_test.js (1 hunks)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1 hunks)
  • src/main/java/konkuk/thip/config/RetryConfig.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/UserCommandPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java (2 hunks)
  • src/main/java/konkuk/thip/user/application/port/out/UserCommandPort.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java (5 hunks)
  • src/main/resources/db/migration/V251120__Add_following_unique_constraint.sql (1 hunks)
  • src/test/java/konkuk/thip/user/concurrency/UserFollowConcurrencyTest.java (1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: buzz0331
Repo: THIP-TextHip/THIP-Server PR: 309
File: src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java:36-44
Timestamp: 2025-09-23T08:31:05.161Z
Learning: buzz0331은 기술적 이슈에 대해 실용적인 해결책을 제시하면서도 과도한 엔지니어링을 피하는 균형감을 선호한다. 복잡도 대비 실제 발생 가능성을 고려하여 "굳이" 불필요한 솔루션보다는 심플함을 유지하는 것을 중요하게 생각한다.
📚 Learning: 2025-07-29T08:11:23.599Z
Learnt from: seongjunnoh
Repo: THIP-TextHip/THIP-Server PR: 109
File: src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java:37-46
Timestamp: 2025-07-29T08:11:23.599Z
Learning: THIP 프로젝트에서 ExceptionDescription 어노테이션은 비즈니스 로직에서 발생하는 커스텀 에러 코드가 있는 API에만 사용하며, bean validation만 수행하는 API에는 사용하지 않는다.

Applied to files:

  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
📚 Learning: 2025-09-01T13:18:13.652Z
Learnt from: seongjunnoh
Repo: THIP-TextHip/THIP-Server PR: 287
File: src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java:8-14
Timestamp: 2025-09-01T13:18:13.652Z
Learning: seongjunnoh는 JpaRepository의 findById 메서드 재정의보다는 도메인별 명시적 메서드(findByUserId, findByRoomId 등)를 정의하여 Hibernate Filter 적용을 보장하는 방식을 선호하며, 이를 통해 더 안전하고 의도가 명확한 코드 구조를 구축한다.

Applied to files:

  • src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java
  • src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java
  • src/main/java/konkuk/thip/user/application/port/out/UserCommandPort.java
📚 Learning: 2025-06-29T09:47:31.299Z
Learnt from: seongjunnoh
Repo: THIP-TextHip/THIP-Server PR: 36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.

Applied to files:

  • src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java
📚 Learning: 2025-07-03T03:05:05.031Z
Learnt from: seongjunnoh
Repo: THIP-TextHip/THIP-Server PR: 43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

Applied to files:

  • src/main/java/konkuk/thip/user/application/port/out/UserCommandPort.java
📚 Learning: 2025-10-13T08:39:43.833Z
Learnt from: buzz0331
Repo: THIP-TextHip/THIP-Server PR: 323
File: build.gradle:102-104
Timestamp: 2025-10-13T08:39:43.833Z
Learning: Spring AI 1.0.0-M6에서 Google AI Gemini 전용 스타터가 빈 등록에 실패하는 경우, spring-ai-openai-spring-boot-starter를 사용하고 외부 설정(환경 변수 등)으로 spring.ai.openai.base-url을 Google의 OpenAI 호환 엔드포인트로, spring.ai.openai.api-key를 Google Cloud 액세스 토큰으로, spring.ai.openai.chat.options.model을 Gemini 모델명으로 지정하여 우회할 수 있습니다.

Applied to files:

  • build.gradle
🧬 Code graph analysis (2)
src/test/java/konkuk/thip/user/concurrency/UserFollowConcurrencyTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (35-417)
src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java (1)
src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java (1)
  • Service (29-150)
⏰ 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 (6)
build.gradle (1)

102-107: spring-retry 의존성 추가 방향은 적절합니다 (Retry 설정값만 SLA 기준으로 재확인 권장)

  • Spring Boot BOM을 쓰고 있어서 별도 버전 명시는 없어도 되고, 이미 spring-boot-starter-aop가 포함되어 있어 @Retryable 기반 프록시 구성이 가능한 상태라 의존성 추가 자체는 무난해 보입니다.
  • 이번 팔로우 동시성 이슈가 응답 지연으로 이어지고 있는 상황이니, RetryConfig에 설정한 maxAttempts, backoff, 그리고 JPA 락 타임아웃(5초)과의 조합으로 최악 응답 시간이 얼마까지 길어질 수 있는지 한 번만 계산·검토해 보시면 좋겠습니다.
src/main/java/konkuk/thip/config/RetryConfig.java (1)

6-8: 전역 Retry 설정 클래스는 현재 형태로 충분해 보입니다.

팔로우 동시성 처리에 필요한 @EnableRetry만 노출하고 있어 단순하고 목적에 맞게 구성된 것 같습니다. spring-retry 의존성이 build.gradle 에 누락되지 않았는지만 한 번만 더 확인해 주세요.

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

35-36: RESOURCE_LOCKED 에러 코드 추가가 전체 규칙과 잘 맞습니다.

50000대 시스템/인프라 계열 코드 구간에 배치되어 있고, HttpStatus.LOCKED 및 메시지도 의도(락 경합 시 응답)과 잘 맞아 보입니다. 이 코드가 내려갈 때 클라이언트에서 423 상태 코드에 대한 공통 처리(문구, 재시도 전략 등)가 준비되어 있는지만 한번 점검해 두시면 좋겠습니다.

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

40-46: findByIdWithLock 추가가 기존 패턴과 일관되고 캡슐화가 잘 되어 있습니다.

기존 findById 와 동일한 예외 처리·매핑을 유지하면서 잠금 전용 경로를 메서드로 분리해 둔 점이 좋습니다. UserJpaRepository.findByUserIdWithLock 쪽에서 실제로 필요한 LockMode(예: PESSIMISTIC_WRITE)가 적용되어 있는지만 한 번만 더 확인해 주세요.

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

8-16: user_id + following_user_id 유니크 제약으로 중복 팔로잉 방지가 명확해졌습니다.

엔티티 레벨에서 DB 유니크 제약을 그대로 선언해 두어 의도가 잘 드러나고, Hibernate 가 생성하는 예외 메시지도 일관될 것 같습니다. 마이그레이션(V251120__Add_following_unique_constraint.sql)의 제약 이름과 컬럼 구성과 완전히 동일한지만 한번 더 확인해 두시면 좋겠습니다.

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

22-22: 자기 자신 팔로우 방지는 이 위치에서 처리하는 것이 깔끔합니다.

  • validateParams 에서 userId.equals(targetUserId) 를 바로 막고, USER_CANNOT_FOLLOW_SELF 도메인 에러를 던지는 구조는 명확하고 재시도/락과도 잘 분리되어 있습니다.
  • 현재 형태 그대로 유지해도 충분해 보이며, 추후 검증 항목이 늘어나더라도 이 메서드에 모으는 패턴을 유지하면 가독성이 좋을 것 같습니다.

Also applies to: 83-87

@github-actions
Copy link

github-actions bot commented Nov 24, 2025

Test Results

488 tests   488 ✅  45s ⏱️
145 suites    0 💤
145 files      0 ❌

Results for commit c351557.

♻️ This comment has been updated with latest results.

@buzz0331 buzz0331 changed the title [fix] 팔로잉 동시성 이슈 문제 1차 해결 [fix] 팔로잉 동시성 이슈 문제 1차 해결시도 Nov 24, 2025
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.

2 participants