Skip to content

[Refactor/#152] 학생회비 납부 요청을 이력으로 쌓기 - #156

Open
xeoxxn wants to merge 6 commits into
feat/#151-fee-single-step-requestfrom
refactor/#152-fee-request-history
Open

xeoxxn wants to merge 6 commits into
feat/#151-fee-single-step-requestfrom
refactor/#152-fee-request-history

Conversation

@xeoxxn

@xeoxxn xeoxxn commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

🎯 해결하려는 문제가 무엇인가요?

student_transfer_statuses가 회원당 한 행이라, 반려된 학생이 다시 내면 이전 요청(남은 학기 · 캡처 · 금액 · 안내 문구)을 덮어씁니다. 운영진은 "3학기라고 했다가 반려되자 2학기로 바꿔 다시 낸" 이력을 볼 수 없습니다.

또 같은 회원의 첫 요청이 동시에 두 번 오면 두 번째가 500입니다 (#155의 한계).

❓ 왜 해결해야 하나요?

  • 반려도 기록으로 남겨야 합니다 — 이 테이블을 납부 요청 로그로 쓰기로 했습니다
  • #153에서 members.is_fee_paid를 아웃박스 이벤트로 맞출 때, "어느 요청이 납부 여부를 바꿨는지"를 요청 ID로 가리켜야 합니다
  • 연타 · 재시도로 진행 중 요청이 둘 생기면 운영진이 같은 학생을 두 번 처리하게 됩니다

⭐ 어떻게 해결했나요?

저장 (V26)

  • 회원당 유니크 uk_student_transfer_statuses_member_id 제거
  • 진행 중 요청 유니크 — active_member_id = IF(status IN ('PENDING', 'PAID'), member_id, NULL) 생성 컬럼(VIRTUAL)에 UNIQUE. MySQL UNIQUE는 NULL을 여럿 허용하므로 반려 행은 몇 개든 남고, 진행 중 · 완료 요청은 회원당 하나만 들어갑니다. members.active_student_id와 같은 패턴입니다
  • 최신 요청 조회 인덱스 (member_id, student_transfer_status_id) — 기존 유니크를 지우기 전에 만들어 조회가 인덱스를 잃는 순간이 없게 했습니다
  • 생성 컬럼은 엔티티에 매핑하지 않고 주석만 남겼습니다 (MemberJpaEntity와 같은 방식)

회원의 지금 상태 = 최신 요청 = PK 최대

  • status로는 고를 수 없습니다 — 반려가 여러 개 쌓이고 최신 행 자체가 반려일 수 있습니다. requested_at은 초 단위라 같은 초에 겹칠 수 있습니다
  • 회원 한 명: findFirstByMemberIdOrderByIdDesc / 목록 · 회원별: r.id = (SELECT MAX(r2.id) ... WHERE r2.memberId = r.memberId)

동작

  • 재납부 — 최신 요청이 REJECTED일 때만 새 행을 INSERT합니다. 이전 행은 고치지 않습니다
  • 동시 요청 — 두 요청이 검사를 같이 통과해도 두 번째 INSERT가 유니크에 걸리고, RepositoryImpl.save가 saveAndFlush + ConstraintViolationUtil로 409 FEE_REQUEST_ALREADY_REQUESTED로 바꿉니다 (사물함 LockerApplicationRepositoryImpl과 같은 패턴)
  • 운영진 처리 — 회원의 최신 요청만 바꿀 수 있습니다. 지난 요청이면 409 FEE_REQUEST_NOT_LATEST(신규). 지난 요청을 되살리면 진행 중 요청이 둘이 될 수 있어서입니다
  • 이전 인증 캡처를 지우지 않습니다 — 이력 행이 계속 가리킵니다. FeeSubmitUseCase의 이전 이미지 삭제 로직을 걷어냈습니다

커밋 순서 — 중간 커밋에서도 데이터가 꼬이지 않게 잡았습니다

  1. 유니크 교체 + 위반을 409로
  2. 조회를 최신 요청 기준으로 (이 시점엔 아직 회원당 한 행)
  3. 운영진은 최신 요청만 처리
  4. 이전 캡처 삭제 제거 — 새 행 생성보다 먼저 해야, 이력 행이 가리키는 이미지를 지우는 커밋이 생기지 않습니다
  5. 재납부 시 새 행

🧩 이 PR의 한계 & 트레이드오프

  • 동시 요청의 409 변환은 동시성 테스트로 돌려보지 않았습니다. 사물함과 같은 패턴이라 같은 동작으로 봅니다
  • 관리자 목록은 회원별 최신 요청만 보여줍니다. 한 회원의 요청 이력을 보는 API는 아직 없습니다 (화면에도 없음)
  • 지난 요청이 가리키는 인증 캡처는 계속 남습니다. 학번 · 이름이 찍힌 이미지라 보관 기간 정책이 필요하면 따로 정해야 합니다
  • 검증 범위: MySQL 8.4 컨테이너(ItemOptimisticLockTest — MySqlJpaTest 상속)로 Flyway V20 → V26 실제 적용, ddl-auto=validate 엔티티 검증, 새 JPQL 파싱까지 통과했습니다. ModularityTests · DomainImplAccessTests 통과

⛓️ 기존 기능에 미치는 영향

  • 앱 · 관리자 API 형태는 그대로입니다
  • student_transfer_statuses에 같은 회원의 행이 여러 개 생깁니다. 이 테이블을 member_id로 조회하는 곳은 전부 최신 행 기준으로 바꿨습니다 (payers는 그대로 회원당 1행)
  • 관리자 상태 변경에 지난 요청 ID를 보내면 409 FEE_REQUEST_NOT_LATEST가 납니다 ("지난 요청"은 이 PR에서 처음 생깁니다)
  • 저장이 saveAndFlush라 INSERT · UPDATE가 즉시 나갑니다. 같은 트랜잭션 안이라 원자성은 그대로입니다

🔀 Edge Case & 실패 시나리오

상황 동작
같은 회원이 동시에 두 번 요청 하나만 PENDING, 나머지는 409 FEE_REQUEST_ALREADY_REQUESTED
반려 → 다시 냄 → 또 반려 → 다시 냄 REJECTED 2행 + PENDING 1행. 화면 · 목록은 최신(PENDING)
운영진이 지난 반려 요청 ID로 PATCH 409 FEE_REQUEST_NOT_LATEST
운영진이 최신 REJECTED를 PAID로 정정 허용 — 최신 요청이고 다른 진행 중 요청이 없습니다
기존 데이터(회원당 1행)에 유니크 교체 충돌 없음 — 진행 중 요청이 회원당 하나뿐입니다

📋 검토한 대안과 선택 이유

  • 낙관적 락: 두 요청 모두 기존 행을 고치지 않고 새 행을 넣기만 해서 잠글 행이 없고, 첫 요청은 행이 아예 없습니다
  • 비관적 락 (SELECT ... FOR UPDATE): 첫 요청은 잠글 행이 없습니다. 갭 락으로 막을 수는 있지만 두 요청이 같은 갭을 잡으면 교착으로 끝나기 쉽습니다
  • is_latest 플래그 컬럼: 새 행마다 이전 행 UPDATE가 따라와 쓰기가 둘이 되고, 둘이 어긋날 여지가 생깁니다
  • 최신 요청을 status로 판단: 반려가 여럿이고 최신이 반려일 수 있어 불가능합니다

💬 리뷰 포인트

  • [c] 생성 컬럼 유니크로 동시 요청을 막는 방식
  • [c] 지난 요청 처리를 막은 것 (FEE_REQUEST_NOT_LATEST)
  • [a] saveAndFlush로 바꾼 것 — 운영진 처리 트랜잭션에서도 UPDATE가 즉시 나갑니다

@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Repository: billilge/stream-server/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2384477c-52bc-4180-aedd-51d0473b878f
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

학생회비 납부 요청을 이력으로 쌓기

1 participant