Skip to content

[Feat/#153] 학생회비 납부 여부 변경을 아웃박스에 기록 - #159

Open
xeoxxn wants to merge 2 commits into
refactor/#152-fee-request-historyfrom
feat/#153-fee-status-outbox
Open

xeoxxn wants to merge 2 commits into
refactor/#152-fee-request-historyfrom
feat/#153-fee-status-outbox

Conversation

@xeoxxn

@xeoxxn xeoxxn commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

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

회원의 회비 납부 여부를 members.is_fee_paid로 판정하도록 되돌릴 예정입니다. 빌릴게 대여 · 행사 신청 · 사물함처럼 납부 여부를 묻는 쪽은 같은 요청에서 이미 member를 읽어 영속성 컨텍스트에 올려둔 경우가 많아, 추가 쿼리 없이 판정할 수 있습니다.

그런데 그 값을 바꾸는 쪽(운영진 처리)은 welfare이고 컬럼의 주인은 member입니다. 같은 트랜잭션에서 두 도메인을 함께 고치면, c04398d에서 member 사본을 지운 이유("주인이 아닌 member 도메인에 welfare 상태를 심고, 같은 트랜잭션에서 맞출 책임을 양쪽에 지운다")가 다시 생깁니다.

❓ 왜 해결해야 하나요?

welfare는 "회원의 납부 여부가 바뀌었다"는 사실만 남기고, member가 자기 리스너로 자기 컬럼을 고치게 하려면 그 사실이 먼저 기록돼야 합니다. 리스너와 컬럼은 member PR에서 하고, 이 PR은 기록만 합니다.

⭐ 어떻게 해결했나요?

이벤트 2개 (core:common common.event)

MemberFeePaid(Long memberId, Long studentTransferStatusId, LocalDateTime occurredAt)
MemberFeePaymentCanceled(Long memberId, Long studentTransferStatusId, LocalDateTime occurredAt)
  • 둘로 나눈 이유: MemberFeePaid 하나로는 운영진 정정(납부 취소)을 담을 수 없습니다. 각각이 하나의 사실입니다
  • 상태 문자열을 싣지 않습니다 — member는 "냈냐"만 알면 되고, common에 welfare의 상태 어휘를 들이지 않습니다
  • occurredAt — 같은 회원의 납부 · 취소가 재시도로 순서가 뒤바뀌어 도착할 때 늦은 쪽을 가려내는 데 씁니다

기록 시점 — StudentTransferStatusServiceImpl.review()에서 납부 여부(paid)가 바뀔 때만

전이 이벤트
PENDING → PAID MemberFeePaid
PAID → REJECTED · PAID → PENDING (운영진 정정) MemberFeePaymentCanceled
PENDING → REJECTED, 학생 요청(→ PENDING) 없음 — paid가 그대로 false

운영진은 회원의 최신 요청만 처리할 수 있으므로(#156) 이 요청의 변화가 곧 회원의 변화입니다.

트랜잭션 — review()는 AdminFeeReviewUseCase 트랜잭션에 합류합니다. 상태 UPDATE · 아웃박스 INSERT · payers 동기화가 한 번에 커밋 · 롤백됩니다. OutboxWriter는 MANDATORY라 트랜잭션 밖에서 부르면 바로 실패합니다.

🔁 이 PR을 위해 스택을 갱신했습니다

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

  • 리스너가 아직 없어서, 이벤트는 기록되자마자 릴레이가 PUBLISHED로 처리하고 OutboxCleaner가 7일 뒤 지웁니다. member PR에서 리스너를 붙여도 그 전까지의 변경은 다시 오지 않습니다
  • 순서 보호(이미 반영한 것보다 오래된 이벤트 무시)는 리스너 쪽 일입니다. 이 PR은 판별에 필요한 값만 싣습니다
  • 검증 범위: 전체 테스트 통과 — RentalApplyConcurrencyTest의 서버 전체 컨텍스트 기동 포함(R2 환경변수가 필요한 테스트라 연결하지 않는 더미 값으로 실행), MySQL 8.4 컨테이너 Flyway V26, ModularityTests · DomainImplAccessTests. 이벤트 JSON 왕복은 Jackson 3.1.4로 확인했습니다(occurredAt 마이크로초까지 동일). 이벤트가 실제로 outbox_events에 쌓이는지 확인하는 테스트는 쓰지 않았습니다

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

  • 운영진 상태 처리 트랜잭션에 outbox_events INSERT가 최대 1건 늘어납니다
  • 리스너가 없으므로 그 외 동작 변화는 없습니다. API · 응답 형태도 그대로입니다

🔀 Edge Case & 실패 시나리오

상황 동작
PAID → PAID (문구만 다시 저장 등) 이벤트 없음
REJECTED → REJECTED (안내 문구 수정) 이벤트 없음
payers 동기화가 실패해 롤백 아웃박스 행도 함께 롤백 — 이벤트 없음
지난 요청을 처리하려 함 FEE_REQUEST_NOT_LATEST로 막혀 이벤트 없음
운영진이 납부완료 → 정정 → 다시 납부완료 MemberFeePaid → MemberFeePaymentCanceled → MemberFeePaid 순서로 3건 (occurredAt으로 순서 판별 가능)

📋 검토한 대안과 선택 이유

  • 상태가 바뀔 때마다 발행 (FeePaymentStatusChanged): member가 필요한 건 paid 하나라, 쓸모없는 이벤트와 리스너 분기만 늘어납니다
  • 이벤트 하나에 boolean paid: 가능하지만 "회원이 냈다 / 납부가 취소됐다"가 각각 사실로 읽히는 쪽을 택했습니다
  • 같은 트랜잭션에서 members 직접 수정: c04398d에서 지운 구조로 되돌아갑니다

💬 리뷰 포인트

  • [r] member PR에 is_fee_paid 백필이 반드시 들어가야 합니다 — 회원별 최신 요청이 PAID인지로 한 번에 채웁니다 (위 한계 1번)
  • [c] 이벤트를 둘로 나눈 것과 이름
  • [c] occurredAt에 LocalDateTime을 쓴 것 — 기존 이벤트엔 시각 필드가 없었습니다

@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: 5f6ee07a-685d-440d-a8ab-0109b768c2cc
  • 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