Skip to content

[w06][sujin] Spring Event#41

Open
cl-o-lc wants to merge 7 commits into
mainfrom
sujin/w06
Open

[w06][sujin] Spring Event#41
cl-o-lc wants to merge 7 commits into
mainfrom
sujin/w06

Conversation

@cl-o-lc

@cl-o-lc cl-o-lc commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

이번 주차

  • 주차: w06
  • 도메인: 결제(payment)

진행 상황

  • s1 — publishEvent + @eventlistener 기초 (동기 호출 / @order / 예외 전파 / 인터페이스 vs 어노테이션 / payload-only)
  • s2 — @TransactionalEventListener 4 phase (6주차 핵심)
  • s3 — @async + 스레드풀 / self-invocation 함정
  • s4 — AOP vs Event 결정 매트릭스

환경

  • Java: 21 (Corretto 21.0.6)
  • 스레드 수 / 반복 횟수: 해당 없음 — STAGE 1은 단일 스레드 동기 호출 관찰 (성능 측정은 STAGE 3 @async에서)
  • 측정 도구: println 관찰 + MeasurementLog 자동 누적(measurements.md)
  • OS / CPU: Windows 11

시도한 거 (시간순)

s1 — publishEvent 기초

  1. HelloEvent: 리스너가 publisher와 같은 스레드 동기 실행, return이 리스너 뒤에 출력
  2. 리스너 3개 + @Order — 숫자 작을수록 먼저(C→B→A로 검증)
  3. 리스너 예외 — 동기 시 다음 리스너 미호출 + main까지 전파
  4. ApplicationListener<E> vs @EventListener — 같은 메커니즘
  5. payload-only — ApplicationEvent 상속 없이 String/record 발행

s2 — @TransactionalEventListener 4 phase
6. 2-1: 순진한 @EventListener → 롤백돼도 알림 이미 전송(함정) → AFTER_COMMIT로 해결
7. 2-2: 4 phase를 한 이벤트에 다 붙여 commit/rollback 매트릭스 관찰
8. 2-3: phase 배치 설계 (영수증/포인트/보상/감사) — 결정 기준 도출
9. 2-4: 트랜잭션 밖 publishEvent → fallbackExecution false/true 비교
10. 2-5: BEFORE_COMMIT 영수증(같은 트랜잭션) + AFTER_COMMIT 포인트(REQUIRES_NEW 함정)

s3 — @async
11. 3-1/2: 동기 리스너 vs @Async publisher 블록 시간 측정
12. 3-3: self-invocation — this.asyncMethod()@Async 무시(5주차 회수)
13. 3-6: @Async + AFTER_COMMIT 새 스레드 → 트랜잭션 컨텍스트 유무
14. 3-4/5/7: 풀 함정 / 예외 소멸 / Virtual Thread (3-7 실측)

s4 — AOP vs Event
15. 5주차 AuthAspect(게이트+감사) 분해 → AOP/Event 결정 매트릭스

측정 결과

s1 — 동작 관찰 (기대 vs 실제)

단계 기대 동작 실제 일치
s1-1 같은 스레드, return 뒤 thread=main, return 뒤
s1-2 @order 작은 값 먼저 C→B→A
s1-3 예외 시 L3 미호출 + main 전파 L3 없음, main caught
s1-4 인터페이스/어노테이션 둘 다 발화 둘 다 출력
s1-5 타입별 매칭 타입대로 매칭

s2 — phase 동작

phase 정상 commit 예외 rollback
BEFORE_COMMIT ✗ (commit 시도 안 함)
AFTER_COMMIT ✗ (status 가드)
AFTER_ROLLBACK
AFTER_COMPLETION
  • ★ 실측 발견: after-그룹 내부 순서 비보장 — AFTER_COMPLETIONAFTER_COMMIT보다 먼저 출력(셋 다 같은 afterCompletion(status)
    콜백)
  • 2-4: fallbackExecution=false 조용히 무시 / true 즉시 인라인 실행
  • 2-5: receipt 행=1(BEFORE_COMMIT 같이 commit). point는 REQUIRES_NEW 없이도 행=1로 살아남음 → autoCommit 복구의 우발적
    commit(비이식성)

s3 — @async

케이스 publisher 시간 리스너 스레드
동기 @eventlistener 511ms main
@async 4ms task-N
self-invocation (비동기 X) main
@async+AFTER_COMMIT, REQUIRES_NEW 무 task-1, actualTxActive=false
@async+AFTER_COMMIT, REQUIRES_NEW 유 task-1, actualTxActive=true
  • 3-7 Virtual Thread: isVirtual()=true인데 이름은 task-1 그대로

측정 형식 가이드 → CONTRIBUTING.md

통증 / 막힌 곳

  • s1: 한 패키지 @SpringBootApplication 여러 개 → 스캔 섞임. stage별 이벤트 타입 다르게 + flat 구조로 해결
  • s1: ApplicationListener<E>E extends ApplicationEvent 제약 → payload record는 컴파일 에러
  • 2-5 REQUIRES_NEW "되던데?" 함정 — 내 H2에선 REQUIRES_NEW 없이도 point가 살아남아서 함정이 안 보였음. 원인이 "autoCommit 복구 시 pending INSERT 우발 commit"이라는 걸 파고서야 왜 그래도 위험한지(비이식성/JPA 유실) 이해
  • 3-3 self-invocation: this 호출이 프록시 우회 → @Async 무시. 5주차 @Transactional 함정과 동일
  • 3-6: @Async+AFTER_COMMIT은 새 스레드라 TransactionSynchronizationManager(ThreadLocal) 손실 → DB/영속성 만지면 REQUIRES_NEW 필수

해결 후 인사이트

  • payload-only(@EventListener)는 내부에서 PayloadApplicationEvent<T>로 래핑 → 그래서 인터페이스로 받으려면 ApplicationListener<PayloadApplicationEvent<...>>
  • 동기일 때의 "예외 전파 O / 체인 중단 O"를 기준선으로 잡아둬야 STAGE 2(phase 격리) / STAGE 3(@async 전파 X)에서 뭐가 바뀌는지 비교 가능
  • AFTER_COMMIT = at-most-once 천장 — commit 보장은 되나 commit 직후~리스너 실행 전 크래시 시 부수효과 유실(재처리 없음). 보장
    필요하면 Transactional Outbox + MQ로 at-least-once(+멱등)
  • REQUIRES_NEW가 두 함정의 공통 정답 — AFTER_COMMIT DB 쓰기(2-5) + @async 새 스레드(3-6) 둘 다 "새 트랜잭션을 명시적으로 열어 자기
    commit"으로 해결
  • phase는 "commit/rollback 기준 그룹 실행 시점"만 보장, 같은 after-그룹 내부 순서는 의존 금지
  • AOP vs Event는 직교 — 메서드를 가로채 제어(veto/wrap)하면 AOP, 한 사건이 여러 모듈로 fan-out하면 Event. 권한 거부는 본문 진입
    X라 이벤트로 못 잡음(→ 게이트·감사 모두 AOP)

회고

  • 발행/구독 메커니즘을 가장 작은 단위부터 손으로 익힘. publishEvent 한 줄이 멀티캐스터→리스너 순차 호출이라는 게 눈에 들어옴
  • 5주차 양파(advice 안-밖)의 "commit 전 실행" 한계를 6주차 시간축 phase로 푸는 흐름이 손에 들어옴
  • "되던데?"가 제일 위험한 함정(2-5)에서 환경 의존을 의심하는 습관을 배움 — 1 나와도 위험한 이유를 설명할 수 있는 게 차별점
  • 다음 7주차 JPA에서 @DomainEvents / 영속성 컨텍스트(3-6 LazyInit 예고)로 자연 연결

cl-o-lc added 7 commits June 8, 2026 20:27
- 2-1 naive @eventlistener fires on rollback; AFTER_COMMIT cancels it
- 2-2 phase matrix; after-group order not guaranteed (shared afterCompletion)
- 2-4 fallbackExecution=false silently drops listener outside a tx
- 2-5 AFTER_COMMIT insert survived w/o REQUIRES_NEW via autoCommit-restore commit on H2 — non-portable, JPA would lose it; REQUIRES_NEW required
@cl-o-lc cl-o-lc self-assigned this Jun 10, 2026
@cl-o-lc
cl-o-lc marked this pull request as ready for review June 10, 2026 17:02
@cl-o-lc cl-o-lc changed the title [w06][sujin] Spring Event — publishEvent + @EventListener 기초 [w06][sujin] Spring Event Jun 10, 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