Skip to content

[w06][minseo] Spring Event 기초 — 동기 실행, 순서 제어 및 예외 전파 메커니즘 분석#39

Open
minseokim0113 wants to merge 4 commits into
mainfrom
minseo/w06
Open

[w06][minseo] Spring Event 기초 — 동기 실행, 순서 제어 및 예외 전파 메커니즘 분석#39
minseokim0113 wants to merge 4 commits into
mainfrom
minseo/w06

Conversation

@minseokim0113

@minseokim0113 minseokim0113 commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

📅 이번 주차 개요

  • 주차: w06
  • 도메인: 재고 변경 (Inventory Change) — 재고 차감 후 알림 발송 및 외부 시스템 동기화 시나리오

🏃‍♂️ 진행 상황

  • STAGE 1-1: publishEvent + @EventListener 기본 메커니즘 확인 (동기/동일 스레드)
  • STAGE 1-2: 다중 리스너와 @Order를 통한 순서 제어 확인
  • STAGE 1-3: 리스너 예외 발생 시 후속 리스너 실행 중단 및 예외 전파 확인
  • STAGE 1-4: 옛 방식(ApplicationListener 인터페이스)과 새 방식(@EventListener) 비교
  • STAGE 1-5: Payload-only 이벤트 (record, String 발행) 확인
  • STAGE 2-1: @TransactionalEventListener를 통한 트랜잭션-이벤트 정합성 해결
  • STAGE 2-2: 트랜잭션 4 Phase(BEFORE, AFTER, ROLLBACK, COMPLETION) 실행 매트릭스 분석
  • STAGE 2-3: 트랜잭션 밖 이벤트 처리를 위한 fallbackExecution 학습
  • STAGE 2-4: AFTER_COMMIT 리스너 내 DB 쓰기 함정(Silent Fail) 발견 및 해결
  • STAGE 3: @async 비동기 처리 및 Java 21 Virtual Thread 성능 최적화
  • STAGE 4: AOP(5주차)와 Event(6주차) 아키텍처 비교 및 혼합 전략 수립

🛠️ 개발 환경

  • Language: Java 21
  • Dependencies: spring-boot-starter-web 3.4.1, starter-data-jpa, starter-jdbc, postgresql
  • Infrastructure: MeasurementLog (5주차 자동 파일 저장 로직 이식 버전)
  • Database: PostgreSQL & H2 (트랜잭션 시뮬레이션용)
  • Framework: Spring Boot 3.4.1
  • Concurrency: Java 21 Virtual Threads 활성화 (spring.threads.virtual.enabled=true)

⏳ 타임라인 및 시도한 내용

  1. STAGE 1-1 수행: HelloEvent 발행 시 [publisher] 시작 -> [listener] 실행 -> [publisher] 끝 순서 관찰. 발행자와 리스너가 restartedMain 스레드에서 동기적으로 동작함을 확인.
  2. 인프라 업데이트: 5주차의 MeasurementLog 기능을 이식하여 measurements.md 자동 기록 기능 활성화.
  3. STAGE 1-2 수행: 3개의 리스너(L1, L2, L3)에 @Order를 부여하여 1 -> 2 -> 3 순차 실행 보장 확인.
  4. STAGE 1-3 수행: 중간 리스너(L2)에서 RuntimeException 투척 시 L3 호출이 생략되고 발행자(Publisher)의 catch 블록으로 예외가 전파됨을 확인.
  5. STAGE 1-4 수행: ApplicationEvent 상속 기반의 옛 방식 리스너와 어노테이션 기반 현대 방식의 호환성 및 유연성 비교.
  6. STAGE 1-5 수행: recordString 타입을 활용하여 상속 없이도 이벤트 발행/구독이 가능한 Payload-only 메커니즘 관찰.
  7. STAGE 2-1 수행: 롤백 시에도 알림이 발송되는 정합성 문제를 @TransactionalEventListener(phase = AFTER_COMMIT)로
    해결. 5주차 Advice 안-밖 구조의 한계를 시간축 분리로 극복.
  8. STAGE 2-4 수행: 커밋 후 리스너에서 DB 쓰기가 무시되는 'Silent Fail' 현상 재현. REQUIRES_NEW를 통해 새 트랜잭션을
    강제하여 영속성 보장 성공.
  9. STAGE 3-1, 3-2 수행: 동기 리스너로 인한 624ms 지연을 @async 도입으로 2ms까지 단축. 작업의 주체(Thread) 분리를 통한 응답성 확보.
  10. STAGE 3-5 수행: Java 21 가상 스레드 적용 후 isVirtual: true 확인. OS 스레드 1:1 매칭의 오버헤드를 줄이는 M:N 매칭
    구조 학습.
  11. STAGE 4-1, 4-2 수행: AOP(시도 기록)와 Event(성공 확정 기록)의 차이를 도메인에 적용. 두 기술이 직교(Orthogonal)함을 이용한 하이브리드 감사 시스템 구축.

📊 측정 결과 (Stage 별 리스너 호출 동작)

Stage 관찰 결과 결론
1-1 (Basic) 발행자 시작 -> 리스너 -> 발행자 끝 동기(Synchronous) 방식
1-2 (Order) L1(1) -> L2(2) -> L3(3) @Order로 순서 제어 가능
1-3 (Exception) L1 -> L2(Fail) -> Publisher(Catch) L3 실행 안 됨 (예외 전파됨)
1-5 (Payload) record, String 모두 정상 수신 ApplicationEvent 상속 불필요
Case 관찰 결과 결론
2-1 (Consistency) 롤백 발생 시 알림 발송 자동 취소 트랜잭션 결과와 부수 효과 동기화 성공
2-4 (Silent Fail) AFTER_COMMIT + update 시 저장 안 됨 REQUIRES_NEW 필수 (새 트랜잭션 필요)
3-2 (Performance) Publisher 응답 속도: 624ms → 2ms 비동기 처리를 통한 시스템 응답성 혁신
3-3 (Proxy Trap) this.method() 호출 시 비동기 무시 @Async 또한 프록시 기반 (Self-invocation 제약)
3-5 (Virtual) isVirtual: true 로그 확인 가벼운 스레드 스케줄링으로 I/O Bound 최적화

💥 통증 / 막힌 곳 (Troubleshooting)

1. IntelliJ Gradle 인식 이슈
  • 문제: minseo 폴더가 단순 디렉토리로 인식되어 실행 버튼이 안 뜨는 문제 발생.
  • 해결: build.gradle 추가 및 Gradle 프로젝트 Reload를 통해 소스 루트로 정상 인식시킴.
2. 인프라 일관성 문제
  • 문제: 6주차 초기 MeasurementLog가 콘솔 출력 전용이라 파일 기록이 안 됨.
  • 해결: 5주차 소스에서 파일 저장 로직을 가져와 업데이트 완료.
3. AFTER_COMMIT 내 DB 쓰기 유실 (Silent Fail)
  • 문제: 감사 로그 INSERT 쿼리는 찍히는데 DB에는 데이터가 없는 현상 발생.
  • 원인: 커밋 완료 후 커넥션 동기화는 살아있으나 트랜잭션은 종료된 상태. 커밋 없이 커넥션이 반환됨.
  • 해결: 리스너에 @transactional(propagation = REQUIRES_NEW)를 추가하여 별도 트랜잭션 생성.
4. Virtual Thread 설정 덮어쓰기 이슈
  • 문제: 가상 스레드를 활성화했으나 isVirtual이 계속 false로 나옴.
  • 원인: 직접 등록한 ThreadPoolTaskExecutor 빈이 스프링 부트의 자동 설정을 덮어씀.
  • 해결: 직접 등록한 Executor 빈을 제거하거나 가상 스레드 전용 Executor를 명시하여 해결.

💡 해결 후 인사이트

  • 명시적 발행의 이점: AOP는 특정 포인트컷에 "몰래" 끼어들지만, Event는 publishEvent를 통해 부수 효과가 발생함을 코드상에 명시적으로 드러냄.
  • 동기 방식의 리스크: 리스너 하나에서 발생한 예외가 전체 비즈니스 로직(Publisher)을 중단시킬 수 있음. 이는 실무에서 비동기(@Async) 전환이나 예외 처리 전략이 왜 중요한지 보여줌.
  • 유연한 타입 매칭: 스프링의 multicaster는 이벤트의 타입을 보고 적절한 리스너를 찾아주므로, 복잡한 상속 계층 없이도 필요한 데이터를 전달하기 매우 편리함.
  • 기술 선택이 곧 비즈니스 정책: AOP는 '시도'를 기록하고, Event는 '확정'을 기록한다. 어떤 시점을 선택하느냐가 시스템의 감사 정책을 결정함.
  • 이벤트 기반 디커플링의 가치: 비동기 이벤트를 통해 서비스는 로직에만 집중하고, 부수 효과(알림, 통계, 로그)는 각자의 스레드에서 처리되는 깔끔한 책임 분리를 경험함.
  • 현대적 동시성 모델: 가상 스레드와 이벤트의 조합이 대규모 I/O 처리가 필요한 현대 서버 아키텍처에서 얼마나 강력한지 체감함.

📝 회고

STAGE 1을 통해 스프링 이벤트가 내부적으로 어떻게 리스너를 찾아 호출하는지, 그리고 기본적으로는 발행자와 생명주기를 같이하는 **"동기 호출"**임을 명확히 이해했다.

6주차 과정을 통해 스프링 이벤트 시스템이 단순한 '신호 전달'을 넘어, 트랜잭션의 한계를 극복하고 시스템의 성능을 비약적으로 끌어올리는 핵심 도구임을 배웠다.

특히 5주차에 고민했던 "트랜잭션 안에서 부수 효과를 어떻게 처리할 것인가"에 대한 명쾌한 해답을 찾을 수 있었다. 단순히 코드를 짜는 법을 넘어, 운영체제 수준의 스레드 모델(1:1 vs M:N)과 데이터 정합성 보장 전략을 동시에 고민해 볼 수 있었던 매우 밀도 높은 시간이었다.

@minseokim0113
minseokim0113 marked this pull request as ready for review June 10, 2026 17:17
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