Skip to content

[w06][chanhyeok] 본인 도메인 송금 — Stage1~3 (한계 재현 → AFTER_COMMIT 해결)#38

Open
ochanhyeok wants to merge 3 commits into
mainfrom
ochanhyeok/w06-chanhyeok
Open

[w06][chanhyeok] 본인 도메인 송금 — Stage1~3 (한계 재현 → AFTER_COMMIT 해결)#38
ochanhyeok wants to merge 3 commits into
mainfrom
ochanhyeok/w06-chanhyeok

Conversation

@ochanhyeok

@ochanhyeok ochanhyeok commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

이번 주차

  • 주차: w06
  • 도메인: 송금 완료 — 5 주차 P2P 송금 + 분산락 AOP 의 자연스러운 연장. @Audited 제거 + publishEvent + AFTER_COMMIT listener 로 commit 후 처리 분리

진행 상황

  • 운영자 example STAGE 1 (publishEvent + @eventlistener) 5 개 main 실행 / 관찰
  • 운영자 example STAGE 2 (4 phase / fallbackExecution / AfterCommitDbWrite) 4 개 main 실행
  • STAGE 1 ~ 2 진입 관문 입으로 답 (5 주차 advice 안-밖 한계 / 4 phase 매핑 / @async self-invocation = @transactional 과 같은 프록시 메커니즘)
  • 본인 도메인 Stage1Hello — TransferService 변환 (@Audited → publishEvent + 동기 listener)
  • 본인 도메인 Stage2BeforeCommitTrap — 정상 + 실패 → 한계 재현
  • 본인 도메인 Stage3AfterCommit — @TransactionalEventListener(AFTER_COMMIT) 해결
  • 운영자 STAGE 3 (@async + ThreadPoolTaskExecutor) — 내일
  • 본인 Stage4 ~ 6 (AllPhases / @async / AOP + Event) — 내일

환경

  • Java: 21 (Temurin 21.0.11)
  • 의존성: spring-boot-starter 3.2.0 + starter-aop + starter-jdbc + lettuce-core 6.3.0 + postgresql 42.7.4
  • 컴파일 옵션: `-parameters` (SpEL 메서드 인자 참조 — 5 주차에서 이어옴)
  • DB / 분산락: 5 주차 docker-compose 그대로 (PostgreSQL 5433 + Redis 6379)
  • 측정 도구: `System.out` 로그 + balance / fee 정합성 검증

시도한 거 (시간순)

  1. terms.md 카테고리별 학습 (Event 본질 / 4 phase / @async / Virtual Thread / 신뢰성 / self-invocation 회수 / AOP vs Event)
  2. 운영자 STAGE 1 5 개 main 실행 — multicaster / @order / 예외 전파 / payload-only 본질 관찰
  3. 운영자 STAGE 2 4 개 main 실행 — 4 phase 콜백 구조 / fallbackExecution 함정 / REQUIRES_NEW DB 쓰기
  4. 본인 도메인 폴더 스캐폴드 — 5 주차 인프라 (MeasurementLog / SchemaBootstrap / RedisConfig) + 분산락 (DistributedLock / Aspect / P2PWallet) 복사
  5. 5 주차 SpEL 표기 버그 발견 — `#{fromId}` 가 root object property 로 해석돼 null pointer 에러. `#{#fromId}` (variable reference) 로 정정 (5 주차 코드도 같이)
  6. TransferCompletedEvent record + TransferService 변환 (@Audited 제거 + @transactional + DataSourceUtils + publishEvent)
  7. TransferEventListeners (@eventlistener 동기) → Stage1Hello 실행 / 관찰
  8. `transferWithFailure` 추가 (publishEvent 후 RuntimeException) → Stage2BeforeCommitTrap → 한계 재현
  9. listener 클래스 분리 (@component 제거 + Stage 마다 @bean 등록) → TransferAfterCommitListeners 새 클래스
  10. Stage3AfterCommit 작성 → 어노테이션 한 줄 변경으로 해결 확인

측정 결과

Stage 별 listener 호출 동작

Stage (1) 정상 송금 (2) 실패 송금 (rollback)
Stage1Hello [AUDIT] [NOTIFY] thread=main (실패 케이스 없음)
Stage2BeforeCommitTrap (동기) [AUDIT] [NOTIFY] commit 전 [AUDIT] [NOTIFY] 호출됨 + rollback ✗
Stage3AfterCommit (AFTER_COMMIT) [AUDIT-AC] [NOTIFY-AC] commit 후 listener 호출 X ✓

balance / fee 정합성 (세 Stage 모두 동일)

항목 비고
id=1 잔액 999890 1000000 - 100 - 10 (정상 송금만 반영)
id=2 잔액 1000100 1000000 + 100
fee 누적 10 수수료

핵심 발견 — 실패 케이스에서 동기 listener 는 호출됨 (외부 알림 회수 불가) / AFTER_COMMIT listener 는 자동 스킵 (외부 호출 안전). 어노테이션 한 줄 차이.

통증 / 막힌 곳

  • IntelliJ 컴파일러가 `-parameters` 옵션 기본 적용 X → SpEL `#fromId` 가 null 매핑. 터미널 `./gradlew clean run` 으로 우회 (IntelliJ Settings → Compiler 에 `-parameters` 추가도 가능)
  • 5 주차 코드의 @distributedlock SpEL 표기 `wallet:#{fromId}` 가 잘못. `#{...}` 안의 `fromId` 는 root object property (root null = NullPointerException). `#{#fromId}` (variable reference) 로 정정 필요 — 5 주차에서도 발견 못 한 버그
  • listener 가 @component 자동 scan 되면 Stage 마다 다른 phase listener 분리 불가 → @component 제거 + Stage 마다 @bean 등록 패턴으로 해결

해결 후 인사이트

  • 컨테이너가 빈에 해주는 일 = 주입 / 라이프사이클 / (필요시) 프록시 래핑. publishEvent 가 작동하는 이유는 프록시 때문이 아니라 publisher 가 주입됐기 때문. AOP 어노테이션 (@transactional / @async) 이 있을 때만 프록시 래핑
  • multicaster = publisher 와 listener 사이의 분배 본체. publishEvent → multicaster.multicastEvent → for 루프로 listener 순차 호출
  • @eventlistener vs ApplicationListener 인터페이스 = 내부 메커니즘 동일 (둘 다 ApplicationListener). 차이는 편의성과 기능 통합
  • 4 phase 콜백 구조 = phase 4 개지만 콜백은 3 개 (beforeCommit / afterCommit / afterCompletion). AFTER_COMMIT 과 AFTER_COMPLETION 은 다른 콜백 → AFTER_COMPLETION 이 마지막. AFTER_ROLLBACK 과 AFTER_COMPLETION 은 같은 콜백 → 순서 미보장
  • fallbackExecution 함정 = listener 호출 조건 = publisher 가 트랜잭션 안일 것. 기본 (false) → 트랜잭션 밖 발행 = 조용히 무시. self-invocation 으로 @transactional 우회되면 listener 무시 — 5 주차 함정과 합쳐짐
  • 5 주차 advice 안-밖 한계 → 6 주차 시간축 4 phase. 어노테이션 한 줄 변경 (@eventlistener → @TransactionalEventListener(AFTER_COMMIT)) 으로 외부 호출 안전성 확보

회고

5 주차에서 advice 안-밖 구조로 못 다뤘던 시간축을 6 주차 phase 로 정확히 풀어내는 흐름이 가장 큰 깨달음. 본인 도메인 (송금) 에서 Stage2 → Stage3 어노테이션 한 줄 변경으로 실패 케이스 외부 호출 안전성이 확보되는 걸 직접 확인.

5 주차 SpEL 버그가 본인 도메인 첫 실행에서 발견된 게 의외 — 5 주차 본인 학습 때 본인 도메인 stage 실제 실행 안 했거나 다른 인자 패턴이라 안 걸렸을 듯. 학습 PR 이 단순 코드 머지가 아니라 실제 실행 검증을 동반해야 함을 다시 확인.

내일 = 운영자 STAGE 3 (@async + ThreadPoolTaskExecutor) + 본인 Stage4 ~ 6 (AllPhases / @async / AOP + Event 함께).

🤖 Generated with Claude Code

ochanhyeok and others added 3 commits June 8, 2026 02:48
@distributedlock 의 SpEL 에서 root object property 가 아니라 메서드 인자 variable
참조로 명시 (#fromId). 6 주차 학습 중 본인 도메인 첫 실행 시 발견.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5주차 분산락 도메인 표 옆 4주차 칸 빈 자리 채움.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 주차 chanhyeok 의 P2P 송금 + 분산락 AOP 도메인 연장.

- Stage1Hello — @transactional + publishEvent + 동기 @eventlistener 가장 작은 단위
- Stage2BeforeCommitTrap — 정상 + 실패 → 5 주차 한계 재현
    (DB rollback 됐지만 [AUDIT] [NOTIFY] 이미 호출됨)
- Stage3AfterCommit — @TransactionalEventListener(AFTER_COMMIT)
    rollback 시 listener 자동 스킵 → 외부 호출 안전성 확보

학습 본질 = 5 주차 advice 안-밖 한계 → 6 주차 시간축 4 phase 로 해결.

확장 예정 (다음 학습)
- Stage4 AllPhases (BEFORE_COMMIT / AFTER_ROLLBACK / AFTER_COMPLETION 추가)
- Stage5 @async (listener 별 스레드 + 격리)
- Stage6 AOP + Event 함께

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ochanhyeok ochanhyeok changed the title feat(w06/chanhyeok): 본인 도메인 송금 Stage1~3 + w05 SpEL 정정 [w06][chanhyeok] 본인 도메인 송금 — Stage1~3 (한계 재현 → AFTER_COMMIT 해결) Jun 7, 2026
@ochanhyeok ochanhyeok self-assigned this Jun 7, 2026
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