Search
Duplicate
πŸ₯…

μŠ€ν”„λ§ μ΄λ²€νŠΈμ™€ λ™μž‘μ›λ¦¬

생성일
2023/04/30
νƒœκ·Έ
Spring
Event
Transaction
Refactoring

λͺ©μ°¨

λ°°κ²½

ν˜„μž¬ 저희 μ„œλΉ„μŠ€μ—μ„œ νˆ¬ν‘œ/μ•½μ†μž‘κΈ° 등을 μƒμ„±ν•˜λ©΄ μŠ¬λž™μœΌλ‘œ μ•Œλ¦Όμ„ λ³΄λ‚΄μ£ΌλŠ” 둜직이 μžˆμŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ μ•Œλ¦Ό λ‘œμ§μ„ μœ„ν•΄ νˆ¬ν‘œ/μ•½μ†μž‘κΈ° μ„œλΉ„μŠ€ κ³„μΈ΅μ—μ„œλŠ” μ•Œλ¦Ό μ„œλΉ„μŠ€ 계측을 μ˜μ‘΄ν•˜κ³ , 생성 μ‹œ μ•Œλ¦Όμ„ λ³΄λ‚΄λŠ” λ‘œμ§κΉŒμ§€ ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. 즉 νˆ¬ν‘œ/μ•½μ†μž‘κΈ° 생성, μŠ¬λž™ μ•Œλ¦Όμ΄ ν•˜λ‚˜μ˜ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 내에 μžˆλŠ” 것이죠. μ΄λŠ” λ°”κΏ”λ§ν•˜λ©΄ μ˜μ‘΄λ„κ°€ λ†’λ‹€κ³  ν•  수 μžˆμŠ΅λ‹ˆλ‹€. ν˜„μž¬μ˜ λ‘œμ§μ—μ„œ λ°œμƒν•  수 μžˆλŠ” 문제점과 ν•΄κ²° λ°©λ²•μœΌλ‘œ μ‚¬μš©ν•œ μŠ€ν”„λ§ 이벀트 λ°©μ‹μ˜ λ™μž‘ 원리λ₯Ό μ•Œμ•„λ³΄κ² μŠ΅λ‹ˆλ‹€.

문제점

ν˜„μž¬ νˆ¬ν‘œμ˜ 생성 λ‘œμ§μ„ λ‹¨μˆœν™”ν•˜λ©΄ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.
@Transactional public PollResponse createPoll(String teamCode, Long memberId, PollCreateRequest request) { Poll poll = request.toPoll(teamCode, memberId, systemTime.now()); Poll savedPoll = pollRepository.save(poll); notificationService.notifyPollOpen(teamCode); return PollResponse.from(memberId, savedPoll); }
Java
볡사
νˆ¬ν‘œ 생성 save(poll)κ³Ό μ•Œλ¦ΌnotifyPollOpen(teamCode)이 ν•˜λ‚˜μ˜ 둜직으둜 λ¬Άμ—¬μžˆλŠ”λ°μš”, μ΄λ ‡κ²Œ λ‹€λ₯Έ μ»¨ν…μŠ€νŠΈκ°€ μ„œλ‘œ μ˜μ‘΄λ˜μ–΄ μžˆλŠ” 상황을 κ°•ν•˜κ²Œ κ²°ν•©λ˜μ–΄ μžˆλ‹€κ³  ν•  수 있고 결합도가 높은 μœ„μ˜ λ‘œμ§μ€ 크게 μ„Έ κ°€μ§€μ˜ λ¬Έμ œμ μ„ κ°€μ§‘λ‹ˆλ‹€.
1.
μœ μ§€, λ³΄μˆ˜μ— 어렀움
μ•Œλ¦Ό μ„œλΉ„μŠ€μ˜ λ„€μ΄λ°μ΄λ‚˜ μ‹œκ·Έλ‹ˆμ³κ°€ λ°”λ€Œλ©΄ νˆ¬ν‘œ μ„œλΉ„μŠ€, μ•½μ†μž‘κΈ° μ„œλΉ„μŠ€ λ“±μ—μ„œ ν•¨κ»˜ 변경이 λ°œμƒν•˜κ²Œ λ©λ‹ˆλ‹€. νˆ¬ν‘œ μ„œλΉ„μŠ€ μž…μž₯μ—μ„œλŠ” νˆ¬ν‘œ μƒμ„±μ΄λΌλŠ” μ£Ό 관심사가 μ•„λ‹Œ μ•Œλ¦Ό μ„œλΉ„μŠ€ 호좜 둜직이 μ‘΄μž¬ν•˜κΈ°λ•Œλ¬Έμ— 가독성 μΈ‘λ©΄μ—μ„œλ„ λΆˆνŽΈν•¨μ΄ μžˆμŠ΅λ‹ˆλ‹€.
2.
μ–½ν˜€μžˆλŠ” νŠΈλžœμž­μ…˜
μ•Œλ¦Ό μ„œλΉ„μŠ€μ—μ„œ μ˜ˆμ™Έκ°€ λ°œμƒν•˜κ²Œ λ˜μ—ˆμ„ λ•Œ, μ•Œλ¦Ό μ„œλΉ„μŠ€μ˜ νŠΈλžœμž­μ…˜μ΄ REQUIRED 라면 잘 μƒμ„±λœ νˆ¬ν‘œμ— λŒ€ν•΄μ„œλ„ 둀백이 λ˜λŠ” 상황이 λ°œμƒν•©λ‹ˆλ‹€.
이λ₯Ό ν”Όν•˜κΈ° μœ„ν•΄ μ•Œλ¦Ό λ‘œμ§μ—μ„œ REQUIRES_NEW 둜 μ„€μ •ν•˜μ—¬ μ•Œλ¦Ό μ„œλΉ„μŠ€μ—μ„œ μ˜ˆμ™Έ μ „νŒŒκ°€ λ˜μ§€ μ•Šλ„λ‘ λ‘œμ§μ„ κ΅¬μ„±ν•˜λŠ” 방법이 있긴 ν•˜μ§€λ§Œ, REQUIRES_NEW λŠ” 컀λ„₯μ…˜μ„ μƒˆλ‘œ 가져와 νŠΈλžœμž­μ…˜μ„ μƒˆλ‘œ 생성해야 ν•˜κΈ°λ•Œλ¬Έμ—, μš”μ²­ 처리 μ‹œκ°„μ΄ κΈΈμ–΄μ§ˆ μˆ˜λ°–μ— μ—†μŠ΅λ‹ˆλ‹€.
λ§Œμ•½ μ•Œλ¦Όμ—μ„œ μ‹€νŒ¨ν•  μ‹œμ— νˆ¬ν‘œ 생성도 μ‹€νŒ¨ν•΄μ•Ό ν•œλ‹€λŠ” λΉ„μ¦ˆλ‹ˆμŠ€ 정책이 μžˆλ‹€λ©΄ 두 λ‘œμ§μ€ λ™μΌν•œ νŠΈλžœμž­μ…˜μ„ νƒ€μ•Όν•˜μ§€λ§Œ, μ €ν¬λŠ” νˆ¬ν‘œ 생성과 μ•Œλ¦Όμ€ κ°œλ…μƒ λ‹€λ₯Έ κΈ°λŠ₯이라고 νŒλ‹¨ν•˜μ—¬ 두 κΈ°λŠ₯ 사이 νŠΈλžœμž­μ…˜μ΄ μ–½ν˜€μžˆμ§€ μ•Šκ³  λ…λ¦½λœ 두 개의 νŠΈλžœμž­μ…˜μ΄ κ΅¬μ„±λ˜μ–΄μ•Ό ν•œλ‹€κ³  νŒλ‹¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
3.
λΆˆν•„μš”ν•œ λŒ€κΈ°
μ˜ˆμ™Έκ°€ λ°œμƒν•˜μ§€ μ•ŠλŠ”λ‹€κ³  ν•˜λ”λΌλ„, μŠ€ν”„λ§μ—μ„œ μš”μ²­μ€ 기본적으둜 단일 μ“°λ ˆλ“œμ—μ„œ λ™μž‘ν•˜κ²Œ 되고 ν•˜λ‚˜μ˜ μ“°λ ˆλ“œ λ‚΄λΆ€μ˜ μ½”λ“œλŠ” λ™κΈ°μ μœΌλ‘œ μ²˜λ¦¬λ©λ‹ˆλ‹€. ν˜„μž¬ μ½”λ“œλŠ” νˆ¬ν‘œ 생성 둜직과 μ•Œλ¦Ό 둜직이 λͺ¨λ‘ μ™„λ£Œκ°€ λ˜μ–΄μ•Ό μ‘λ‹΅ν•©λ‹ˆλ‹€. 이 λ•Œ μ•Œλ¦Ό 둜직이 λ¬΄κ±°μ›Œ 였래 κ±Έλ¦°λ‹€λ©΄, νˆ¬ν‘œ 생성과 λ³„κ°œμΈ 둜직으둜 인해 핡심 λ‘œμ§μ— λŒ€ν•œ 응닡이 λŒ€κΈ°ν•˜κ²Œ λ˜λŠ” 상황이 λ°œμƒν•©λ‹ˆλ‹€.

생각해본 λŒ€μ•ˆ

μŠ€ν”„λ§ 배치

β€’
μž₯점
μ•žμ„œ μ–ΈκΈ‰ν•œ λ¬Έμ œμ λ“€μ„ λͺ¨λ‘ ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ•Œλ¦Όμ— λŒ€ν•œ λͺ¨λ“  λ‘œμ§μ€ 배치 둜직으둜 λŒμ•„κ°€κΈ°λ•Œλ¬Έμ—, νˆ¬ν‘œ 생성 λ‘œμ§μ—μ„œ μ•Œλ¦Όμ— λŒ€ν•œ 물리적, 논리적 μ˜μ‘΄μ„±μ„ λͺ¨λ‘ λ–Όμ–΄λ‚Ό 수 μžˆμŠ΅λ‹ˆλ‹€. 이에 따라 νˆ¬ν‘œ 생성 λ‘œμ§μ—μ„œλŠ” 핡심 둜직만 μ²˜λ¦¬ν•˜μ—¬ λΆˆν•„μš”ν•œ λŒ€κΈ°λ₯Ό 쀄일 수 있으며 두 λ‘œμ§μ€ μ™„μ „νžˆ λ…λ¦½λœ νŠΈλžœμž­μ…˜μ—μ„œ λ™μž‘ν•˜κ²Œ λ©λ‹ˆλ‹€. μ΄λŸ¬ν•œ μž₯점과 λ”λΆˆμ–΄ 배치만의 μž₯점이 μžˆλŠ”λ°μš”, μ™„μ „νžˆ λ…λ¦½λœ 둜직으둜 κ΅¬μ„±λ˜μ–΄ μžˆκΈ°λ•Œλ¬Έμ— μ•Œλ¦Όμ—μ„œ λ°œμƒν•˜λŠ” μ˜ˆμ™Έμ— λŒ€ν•΄ μ•ˆμ •μ μœΌλ‘œ μ²˜λ¦¬ν•  수 있고, μ‹€νŒ¨ν•œ μ•Œλ¦Όμ— λŒ€ν•΄ μΆ”ν›„ μž¬μ²˜λ¦¬κ°€ κ°€λŠ₯ν•˜λ‹€λŠ” μ μž…λ‹ˆλ‹€.
β€’
단점
μ•„λ¬΄λž˜λ„ λŒ€κ·œλͺ¨ 처리λ₯Ό μœ„ν•œ μž‘μ—…μ΄λ‹€λ³΄λ‹ˆ, μŠ€μΌ€μ€„μ΄ 자주 λŒμ§€ μ•Šμ•„ μ‹€μ‹œκ°„μ„±μ΄ λΆ€μ‘±ν•©λ‹ˆλ‹€. λ˜ν•œ 배치 μž‘μ—…μ„ μœ„ν•΄ 컬럼, ν…Œμ΄λΈ” λ“± 뢀가적인 데이터 정보λ₯Ό DB 에 μ €μž₯ν•΄μ•Ό ν•©λ‹ˆλ‹€.

μ“°λ ˆλ“œ 생성

β€’
μž₯점
μ“°λ ˆλ“œλ₯Ό μƒμ„±ν•˜μ—¬ λ‹€λ₯Έ μžμ›μ˜ μΆ”κ°€ 없이 μ‰½κ²Œ 적용이 κ°€λŠ₯ν•©λ‹ˆλ‹€.
private void notifyPollOpen(String teamCode) { Thread thread = new Thread(() -> notificationService.notifyTeamPollOpen(teamCode)); thread.start(); }
Java
볡사
λ˜ν•œ λΉ„λ™κΈ°μ μœΌλ‘œ μ²˜λ¦¬κ°€ κ°€λŠ₯ν•˜μ—¬ 메인 둜직인 νˆ¬ν‘œ 생성 λ‘œμ§μ€ λΆˆν•„μš”ν•œ λŒ€κΈ°λ₯Ό 쀄일 수 있으며, 메인 둜직과 λ‹€λ₯Έ μ“°λ ˆλ“œμ΄κΈ°λ•Œλ¬Έμ— λ…λ¦½λœ νŠΈλžœμž­μ…˜μœΌλ‘œ 둜직이 μ§„ν–‰λ©λ‹ˆλ‹€.
β€’
단점
μ•Œλ¦Ό λ‘œμ§μ„ 직접 μ“°λ ˆλ“œλ‘œ μƒμ„±ν•˜μ—¬ μ§„ν–‰ν•˜λ‹€λ³΄λ‹ˆ, μƒμ„±ν•œ μ“°λ ˆλ“œ λ‚΄λΆ€μ—μ„œ κ²°κ΅­ μ•Œλ¦Ό 도메인에 λŒ€ν•œ μ˜μ‘΄μ„±μ΄ 생길 μˆ˜λ°–μ— μ—†μŠ΅λ‹ˆλ‹€. λ˜ν•œ μ•Œλ¦Ό λ‘œμ§μ—μ„œ λ°œμƒν•˜λŠ” μ˜ˆμ™Έμ— λŒ€ν•΄ μ²˜λ¦¬κ°€ νž˜λ“€λ‹€λŠ” 단점이 μžˆμŠ΅λ‹ˆλ‹€. μ„œλΉ„μŠ€ 계측 λ‚΄λΆ€μ—μ„œ μƒˆλ‘œ μƒμ„±ν•œ μ“°λ ˆλ“œμ΄κΈ°λ•Œλ¬Έμ— λ””μŠ€νŒ¨μ²˜ μ„œλΈ”λ¦Ώμ„ 타지 μ•Šμ•„ 컨트둀러 μ–΄λ“œλ°”μ΄μŠ€μ™€ 같은 spring 이 μ œκ³΅ν•΄μ£ΌλŠ” μ˜ˆμ™Έ 처리 κΈ°λŠ₯을 μ΄μš©ν•  수 μ—†μŠ΅λ‹ˆλ‹€. λ§Œμ•½ 컀밋 μ‹œμ μ—μ„œ νˆ¬ν‘œ 생성 둜직이 λ‘€λ°±λœλ‹€λ©΄, μ•Œλ¦Ό λ‘œμ§λ„ μ‹€ν–‰λ˜μ§€ μ•Šμ•„μ•Ό ν•˜λŠ”λ° μ΄λŸ¬ν•œ 뢀뢄에 λŒ€ν•΄ κ΄€λ¦¬ν•˜κΈ°κ°€ μ–΄λ ΅μŠ΅λ‹ˆλ‹€.

μŠ€ν”„λ§ 이벀트 방식

μ΄λ²€νŠΈλŠ” ν”„λ‘œκ·Έλž¨μ— μ˜ν•΄ κ°μ§€λ˜κ³  처리될 수 μžˆλŠ” λ™μž‘μ΄λ‚˜ 사건을 λ§ν•˜λŠ”λ°, 일반적으둜 이벀트 기반 μ‹œμŠ€ν…œμ€ ν”„λ‘œκ·Έλž¨μ—μ„œ μ²˜λ¦¬ν•΄μ•Ό ν•  μ™ΈλΆ€ ν™œλ™μ΄ μžˆμ„ λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€. μ—¬κΈ°μ„œ ν”„λ‘œκ·Έλž¨μ— μ˜ν•΄ κ°μ§€λ˜λŠ” 사건은 β€œνˆ¬ν‘œ 생성” 이고, μ²˜λ¦¬ν•΄μ•Ό ν•  μ™ΈλΆ€ ν™œλ™μ€ β€œμ•Œλ¦Ό μ„œλΉ„μŠ€β€ 둜 λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€. μŠ€ν”„λ§μ—μ„œλŠ” μŠ€ν”„λ§ 이벀트λ₯Ό μ§€μ›ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
β€’
μž₯점
μ™ΈλΆ€ λ¦¬μ†ŒμŠ€μ—†μ΄ κ°„νŽΈν•˜κ²Œ 이벀트 방식을 κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ 동기/비동기 λͺ¨λ‘ 지원 κ°€λŠ₯ν•˜κ³  νŠΈλžœμž­μ…˜ 컀밋 성곡 여뢀에 따라 μ•Œλ¦Ό 둜직 μ‹€ν–‰ μ—¬λΆ€λ₯Ό λ”°μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€. 즉 μ“°λ ˆλ“œλ₯Ό 직접 μƒμ„±ν•˜λŠ” 것보닀 더 μ„¬μ„Έν•˜κ²Œ λ‘œμ§μ„ λ‹€λ£° 수 μžˆμŠ΅λ‹ˆλ‹€.
β€’
단점
μ“°λ ˆλ“œ 생성과 λ§ˆμ°¬κ°€μ§€λ‘œ μ˜ˆμ™Έ λ°œμƒμ— λŒ€ν•œ μž¬μ²˜λ¦¬κ°€ νž˜λ“­λ‹ˆλ‹€. λ˜ν•œ AOP λ“± λ³΅μž‘ν•œ λ‚΄λΆ€ λ™μž‘ ꡬ쑰λ₯Ό νŒŒμ•…ν•΄μ•Ό ν•  ν•„μš”κ°€ μžˆμŠ΅λ‹ˆλ‹€.
μ €ν¬λŠ” μœ„μ˜ μ„Έ 가지 쀑 μŠ€ν”„λ§ 이벀트λ₯Ό μ μš©ν•˜κΈ°λ‘œ ν–ˆμŠ΅λ‹ˆλ‹€. μ΄μœ λŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

μŠ€ν”„λ§ 이벀트 선택 이유

1.
μ‹€μ‹œκ°„μ„±μ΄ 보μž₯λ˜μ–΄μ•Ό ν•œλ‹€.
저희 μ„œλΉ„μŠ€μ˜ λΉ„μ¦ˆλ‹ˆμŠ€ μ •μ±… 상 νˆ¬ν‘œκ°€ μƒμ„±λ˜λ©΄ λ™μ‹œμ— μŠ¬λž™ μ•Œλ¦Όμ„ 보내야 ν•©λ‹ˆλ‹€. 이에 따라 배치 방식은 μ‹€μ‹œκ°„μ„±μ΄ λΆ€μ‘±ν•˜λ‹€κ³  νŒλ‹¨ν•˜μ—¬ λ°°μ œν•˜μ˜€μŠ΅λ‹ˆλ‹€.
2.
μ›μžμ„±μ΄ 보μž₯λ˜μ–΄μ•Ό ν•œλ‹€.
νˆ¬ν‘œ 생성이 μ„±κ³΅ν•˜λ©΄ μŠ¬λž™ μ•Œλ¦Όμ΄ 보내져야 ν•˜κ³ , νˆ¬ν‘œ 생성에 μ‹€νŒ¨ν•˜λ©΄ μŠ¬λž™ μ•Œλ¦Ό 둜직이 ν˜ΈμΆœλ˜μ§€ μ•Šμ•„μ•Ό ν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ μ“°λ ˆλ“œ 생성 방식은 νˆ¬ν‘œ 생성 컀밋 μ‹œμ  이전에 μŠ¬λž™ μ•Œλ¦Ό λ‘œμ§μ„ μ‹€ν–‰μ‹œν‚€κΈ°λ•Œλ¬Έμ— μ›μžμ„±μ΄ μ§€μΌœμ§€μ§€ μ•Šμ„ μš°λ €κ°€ μžˆμ„ 것이라고 νŒλ‹¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
3.
관심사가 λΆ„λ¦¬λ˜μ–΄μ•Ό ν•œλ‹€.
이벀트 방식을 μ‚¬μš©ν•˜λ©΄ μ•Œλ¦Ό 도메인에 λŒ€ν•œ 물리적 μ˜μ‘΄μ„±μ΄ 뢄리가 될 수 μžˆμŠ΅λ‹ˆλ‹€. 비둝 μŠ€ν”„λ§ 이벀트 λΌλŠ” λΌμ΄λΈŒλŸ¬λ¦¬μ— μ˜μ‘΄μ„±μ΄ 생기긴 ν•˜μ§€λ§Œ μ•Œλ¦Ό 도메인에 λŒ€ν•΄ μ˜μ‘΄μ„±μ„ λΆ„λ¦¬ν•˜λ©΄μ„œ κ°€μ Έμ˜¬ 수 μžˆλŠ” μœ μ§€/보수의 효율이 더 쒋을 것이라고 μƒκ°ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
이제 μŠ€ν”„λ§ 이벀트의 μ‚¬μš©λ²•κ³Ό ν•¨κ»˜, μŠ€ν”„λ§ 이벀트λ₯Ό μ μš©ν•˜λŠ” κ³Όμ •μ—μ„œ λ§Œλ‚¬λ˜ 선택지듀과 선택 μ΄μœ μ— λŒ€ν•΄ μ•Œμ•„λ³΄κ² μŠ΅λ‹ˆλ‹€.

λ°œν–‰

λ°œν–‰μ€ ν”„λ‘œκ·Έλž¨μ— μ˜ν•΄ κ°μ§€λ˜λŠ” 사건이 λ°œμƒν–ˆλ‹€λŠ” 것을 μ•Œλ €μ€λ‹ˆλ‹€. λ°œν–‰ν•  λ•Œ μ΄μš©ν•˜λŠ” ν΄λž˜μŠ€λ‘œλŠ” λŒ€ν‘œμ μœΌλ‘œ ApplicationEventPublisher와 AbstractAggregateRoot κ°€ μžˆμŠ΅λ‹ˆλ‹€.

ApplicationEventPublisher

ApplicationEventPublisher λŠ” μŠ€ν”„λ§ 빈으둜 λ“±λ‘λ˜μ–΄ μžˆκΈ°λ•Œλ¬Έμ— 객체λ₯Ό μ£Όμž…λ°›μ•„ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 일반적으둜 μ„œλΉ„μŠ€ κ³„μΈ΅μ—μ„œ ν•΄λ‹Ή 객체λ₯Ό μ£Όμž…λ°›μ€ λ’€, publishEvent(event) λ©”μ„œλ“œλ₯Ό 톡해 이벀트λ₯Ό λ°œν–‰ν•©λ‹ˆλ‹€.
@RequiredArgsConstructor @Service public class PollService { private final ApplicationEventPublisher applicationEventPublisher; @Transactional public PollResponse createPoll(String teamCode, Long memberId, PollCreateRequest request) { Poll poll = request.toPoll(teamCode, memberId, systemTime.now()); Poll savedPoll = pollRepository.save(poll); applicationEventPublisher.publishEvent(PollEvent.from(teamCode)); return PollResponse.from(memberId, savedPoll); }
Java
볡사
인자둜 λ³΄λ‚΄μ§€λŠ” PollEvent κ°μ²΄λŠ” POJO 객체이고, Dto 처럼 μ•Œλ¦Ό λ‘œμ§μ—μ„œ μ‚¬μš©ν•  λ³€μˆ˜λ₯Ό 객체에 νƒœμ›Œ 보내면 이λ₯Ό μ²˜λ¦¬ν•˜λŠ” λ‘œμ§μ—μ„œ λ°›μ•„ μ‚¬μš©ν•©λ‹ˆλ‹€.

AbstractAggregateRoot

클래슀λͺ…에 Abstract κ°€ μžˆμ§€λ§Œ 좔상 ν΄λž˜μŠ€λŠ” μ•„λ‹ˆκ³ , λ„λ©”μΈμ—μ„œ 이벀트λ₯Ό κ°„λ‹¨ν•˜κ²Œ μ²˜λ¦¬ν•˜κΈ° μœ„ν•œ ν…œν”Œλ¦ΏμœΌλ‘œ λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€. 도메인 주도 κ΄€μ μ—μ„œ λ³΄μ•˜μ„ λ•Œ, 루트 λ„λ©”μΈμ—μ„œ ν•΄λ‹Ή 클래슀λ₯Ό 상속받고 ν•΄λ‹Ή 클래슀의 protected λ©”μ„œλ“œμΈ registerEvent(event) λ₯Ό 톡해 이벀트λ₯Ό λ“±λ‘ν•©λ‹ˆλ‹€.
public class Poll extends AbstractAggregateRoot<Poll> { // ... Poll(Long id, String teamCode) { // ... registerEvent(PollEvent.from(teamCode)); } }
Java
볡사
ν•˜μ§€λ§Œ registerEventλŠ” 이벀트λ₯Ό λ“±λ‘λ§Œ ν•  뿐, λ°œν–‰ν•˜μ§€λŠ” μ•ŠμŠ΅λ‹ˆλ‹€. λŒ€μ‹  λ°œν–‰ν•˜λŠ” μž‘μ—…μ€ AOP λ°©μ‹μœΌλ‘œ λ°œν–‰ν•˜λŠ”λ°μš”, Spring Data JPA Repository의 save, saveAll, delete, deleteAll이 호좜될 λ•Œ 엔티티에 μŒ“μ—¬ μžˆλŠ” 이벀트λ₯Ό λͺ¨λ‘ λ°œν–‰ν•©λ‹ˆλ‹€.
@RequiredArgsConstructor @Service public class PollService { @Transactional public PollResponse createPoll(String teamCode, Long memberId, PollCreateRequest request) { Poll poll = request.toPoll(teamCode, memberId, systemTime.now()); return PollResponse.from(memberId, pollRepository.save(poll)); }
Java
볡사
μ €ν¬λŠ” ApplicationEventPublisher λ₯Ό μ£Όμž…λ°›λŠ” λŒ€μ‹  AbstractAggregateRoot λ₯Ό 상속받아 μ‚¬μš©ν•˜λŠ” κ²ƒμœΌλ‘œ κ²°μ •ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
λ‘˜μ˜ 차이λ₯Ό 관심사 μΈ‘λ©΄μ—μ„œ 생각해볼 수 μžˆμŠ΅λ‹ˆλ‹€. β€˜νˆ¬ν‘œ 생성’ μ΄λΌλŠ” 사건이 λ°œμƒν•œ 것은 λ„λ©”μΈμ—μ„œ μƒμ„±λœ 것이기에 λ„λ©”μΈμ—μ„œ 관리해야 ν•˜λŠ” κ΄€μ‹¬μ‚¬μž…λ‹ˆλ‹€. 즉 β€˜νˆ¬ν‘œ 생성’ μ΄λΌλŠ” μ΄λ²€νŠΈλŠ” μ„œλΉ„μŠ€ 계측 νˆ¬ν‘œ 생성 둜직의 κ΄€μ‹¬μ‚¬λŠ” μ•„λ‹ˆλΌκ³  μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€. μ„œλΉ„μŠ€ 계측 νˆ¬ν‘œ 생성 둜직의 κ΄€μ‹¬μ‚¬λŠ” κ·Έμ € μš”μ²­ νŒŒλΌλ―Έν„°λ₯Ό νˆ¬ν‘œ POJO 객체둜 λ°”κΎΌ λ’€ 이λ₯Ό μ €μž₯ν•˜λŠ” 것 λΏμž…λ‹ˆλ‹€. 이에 따라 β€˜νˆ¬ν‘œ 생성 μ‹œβ€™μ— 무언가λ₯Ό ν•œλ‹€λ©΄ μ΄λŠ” νˆ¬ν‘œ 도메인 객체의 관심사이기에 이벀트 λ°œν–‰ λ‘œμ§μ„ 도메인 객체에 λ„£λŠ”κ²Œ μ μ ˆν•˜λ‹€κ³  νŒλ‹¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

λ°œν–‰ λ™μž‘ 원리

AbstractAggregateRoot 의 λ°œν–‰ν•˜λŠ” 과정을 쑰금 더 μžμ„Ένžˆ λ“€μ—¬λ‹€ λ³΄κ² μŠ΅λ‹ˆλ‹€. Spring Data JPA repository 의 λ©”μ„œλ“œ 쀑 save, saveAll, delete, deleteAll 이 호좜되면 AOP 둜 λ“±λ‘λœ 인터셉터 쀑 EventPublishingMethodInterceptor μ—μ„œ 이벀트 κ΄€λ ¨ λ‘œμ§μ„ μˆ˜ν–‰ν•©λ‹ˆλ‹€.
class EventPublishingMethodInterceptor implements MethodInterceptor { @Override @Nullable public Object invoke(MethodInvocation invocation) throws Throwable { Object result = invocation.proceed(); if (!isEventPublishingMethod(invocation.getMethod())) { return result; } // ... eventMethod.publishEventsFrom(arguments[0], publisher); return result; } private static boolean isEventPublishingMethod(Method method) { return method.getParameterCount() == 1 // && (isSaveMethod(method.getName()) || isDeleteMethod(method.getName())); } private static boolean isSaveMethod(String methodName) { return methodName.startsWith("save"); } private static boolean isDeleteMethod(String methodName) { return methodName.equals("delete") || methodName.equals("deleteAll") || methodName.equals("deleteInBatch") || methodName.equals("deleteAllInBatch"); } }
Java
볡사
isEventPublishingMethod μ—μ„œ save λ˜λŠ” delete 인지 ν™•μΈν•˜μ—¬ λ§žλ‹€λ©΄ publishEventsFrom λ₯Ό ν˜ΈμΆœν•©λ‹ˆλ‹€. publishEventsFrom λ©”μ„œλ“œ λ‚΄λΆ€μ—μ„œλŠ” μ•žμ„œ μ–ΈκΈ‰ν•œ ApplicationEventPublisher 의 publishEvent() λ₯Ό ν˜ΈμΆœν•˜κ²Œ λ©λ‹ˆλ‹€.
κ²°κ΅­ ApplicationEventPublisher λ₯Ό μ£Όμž…λ°›μ•„ μ‚¬μš©ν•˜λŠ” λ‘œμ§μ΄λ‚˜ AbstractAggregateRoot μ—μ„œ AOP λ₯Ό ν™œμš©ν•΄ μ‚¬μš©ν•˜λŠ” 둜직이 ν•œ κ³³μ—μ„œ λ§Œλ‚˜κ²Œ λ˜λŠ” κ²ƒμž…λ‹ˆλ‹€.
κ·Έλ ‡λ‹€λ©΄ λ°œν–‰ν•œ μ΄λ²€νŠΈλŠ” μ–΄λ””μ—μ„œ λ³΄κ΄€λ˜μ–΄ μ–΄λ–»κ²Œ μ²˜λ¦¬λ κΉŒμš”?
ApplicationEventPublisher 의 publishEvent λ©”μ„œλ“œλŠ” AbstractApplicationContext 좔상 ν΄λž˜μŠ€μ— κ΅¬ν˜„λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. publishEvent λ©”μ„œλ“œ λ‚΄λΆ€μ—μ„œ SimpleApplicationEventMulticaster λ₯Ό 거쳐 TransactionListener 에 등둝을 μš”μ²­ν•˜λ©΄ TransactionalApplicationListenerMethodAdapter 의 onApplicationEvent λ©”μ„œλ“œμ—μ„œ 등둝을 ν•©λ‹ˆλ‹€.
public class TransactionalApplicationListenerMethodAdapter extends ApplicationListenerMethodAdapter implements TransactionalApplicationListener<ApplicationEvent> { // ... public void onApplicationEvent(ApplicationEvent event) { if (TransactionSynchronizationManager.isSynchronizationActive() && TransactionSynchronizationManager.isActualTransactionActive()) { TransactionSynchronizationManager.registerSynchronization( new TransactionalApplicationListenerSynchronization<>(event, this, this.callbacks)); } } }
Java
볡사
TransactionSynchronizationManager λŠ” νŠΈλžœμž­μ…˜μ˜ 동기화λ₯Ό λ„μ™€μ£ΌλŠ” κ°μ²΄μΈλ°μš”, ν•΄λ‹Ή λ©”μ„œλ“œμ— ThreadLocal 의 Set 자료ꡬ쑰둜 이루어진 λ³€μˆ˜μ— λ‹΄κΈ°λ©΄ λ°œν–‰μ΄ μ™„λ£Œλ©λ‹ˆλ‹€.
Set 자료 ꡬ쑰의 νƒ€μž…μ€ TransactionSynchronization 인데 ν•΄λ‹Ή κ°μ²΄λŠ” λ°œν–‰μ‹œ 인자둜 보낸 Dto μ„±κ²©μ˜ 객체와 λ¦¬μŠ€λ„ˆλ₯Ό ν•¨κ»˜ λ‹΄κ³  μžˆμŠ΅λ‹ˆλ‹€. 여기에 λ‹΄κΈ΄ TransactionSynchronization κ°μ²΄λŠ” 좔후에 κ΅¬λ…ν•˜λŠ” κ°μ²΄μ—μ„œ 가져와 μ‚¬μš©ν•˜κ²Œ λ˜λŠ”λ°μš”, μ΄λŠ” λ‹€μŒ μ±•ν„°μ—μ„œ μ•Œμ•„λ³΄λ„λ‘ ν•˜κ² μŠ΅λ‹ˆλ‹€.

ꡬ독

ꡬ독은 μ–΄λ…Έν…Œμ΄μ…˜ λ°©μ‹μœΌλ‘œ κ°„λ‹¨ν•˜κ²Œ 적용이 κ°€λŠ₯ν•˜λ©°@EventListener,Β @TransactionalEventListenerΒ μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ 이벀트 ꡬ독을 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
1.
@EventListener : μ΄λ²€νŠΈκ°€ λ°œν–‰λ˜μ—ˆμ„ λ•Œ 항상 이λ₯Ό μˆ˜μ‹ ν•©λ‹ˆλ‹€.
2.
@TransactionalEventListener : 이벀트λ₯Ό λ°œν–‰ν•˜λŠ” νŠΈλžœμž­μ…˜μ΄ μ˜¬λ°”λ₯΄κ²Œ μˆ˜ν–‰λ˜μ—ˆμ„ λ•Œ μˆ˜μ‹ ν•©λ‹ˆλ‹€.

@TransactionalEventListener 선택 이유

저희 ν”„λ‘œμ νŠΈμ—μ„œλŠ” @EventListener λŒ€μ‹  @TransactionalEventListener λ₯Ό μ‚¬μš©ν•˜μ˜€λŠ”λ°μš”, κ·Έ μ΄μœ λ‘œλŠ” νˆ¬ν‘œ 생성 둜직이 μ˜¬λ°”λ₯΄κ²Œ μˆ˜ν–‰λ˜μ—ˆμ„ λ•Œ μŠ¬λž™ μ•Œλ¦Όμ„ λ³΄λ‚΄λŠ” 것이 λΉ„μ¦ˆλ‹ˆμŠ€ μ •μ±…μ΄μ—ˆκΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. 즉, λ§Œμ•½ νˆ¬ν‘œ 생성 μ‹œλ„μ— μ΄ˆμ μ„ 두어, μ΄λŸ¬ν•œ μ‹œλ„κ°€ μžˆμ„λ•Œλ§ˆλ‹€ 성곡 여뢀와 관계없이 이λ₯Ό κ΅¬λ…ν•˜κ³  μ‹Άλ‹€λ©΄ @EventListener λ₯Ό μ‚¬μš©ν•˜μ˜€μ„ κ²ƒμž…λ‹ˆλ‹€. ν•˜μ§€λ§Œ νˆ¬ν‘œκ°€ μ˜¬λ°”λ₯΄κ²Œ μƒμ„±λ˜μ—ˆμ„ λ•Œλ§Œ 이λ₯Ό κ΅¬λ…ν•˜κ³  μ•Œλ¦Όμ„ 보내야 ν•˜κΈ°λ•Œλ¬Έμ— @TransactionalEventListenerλ₯Ό μ‚¬μš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

쑰건 μ„€μ •

β€’
condition
ν˜„μž¬ 저희 λͺ¨λ½ μ„œλΉ„μŠ€ μ •μ±… 상, μŠ¬λž™ μ•Œλ¦Όμ€ νˆ¬ν‘œλ₯Ό 생성할 λ•Œ 뿐만 μ•„λ‹ˆλΌ νˆ¬ν‘œ μ’…λ£Œ μ‹œμ—λ„ λ³΄λƒ…λ‹ˆλ‹€. μ΄λŸ¬ν•œ λ‘œμ§λ“€μ„ μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄, 이벀트λ₯Ό 등둝할 λ•Œ 인자둜 λ³΄λ‚΄μ§€λŠ” 이벀트 POJO 객체에 isClosed λ³€μˆ˜λ₯Ό μ„€μ •ν•˜κ³  μƒμ„±λœ 것인지, μ’…λ£Œν•œ 것인지 μ•Œλ¦Ό 둜직 λ‚΄μ—μ„œ λΆ„κΈ° μ²˜λ¦¬ν•˜μ—¬ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.
@Transactional public class NotificationService { // ... @TransactionalEventListener public void notifyPoll(PollEvent event) { if (!event.isClosed()) { // νˆ¬ν‘œ 생성 μ•Œλ¦Ό return } // νˆ¬ν‘œ μ’…λ£Œ μ•Œλ¦Ό } }
Java
볡사
ν•˜μ§€λ§Œ 이후에 νˆ¬ν‘œ 제λͺ©μ΄ μˆ˜μ •λ˜μ—ˆμ„ λ•Œμ—λ„ μ•Œλ¦Όμ„ λ³΄λ‚΄μ•Όν•˜λŠ” λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 μΆ”κ°€κ°€ λœλ‹€λ©΄ if 뢄기문이 μΆ”κ°€λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€. μ΄λ ‡κ²Œ ν•˜λ‚˜μ˜ λ©”μ„œλ“œ 내에 계속 뢄기문이 μΆ”κ°€κ°€ λœλ‹€λ©΄ μœ μ§€/λ³΄μˆ˜μ—λ„ 쒋지 μ•Šκ³  가독성도 λ–¨μ–΄μ§€κ²Œ λ©λ‹ˆλ‹€. κ·Έλž˜μ„œ @TransactionalEventListener λŠ” μ΄λŸ¬ν•œ 문제λ₯Ό progmmatic ν•˜κ²Œ μ²˜λ¦¬ν•  수 μžˆλ„λ‘ condition μ˜΅μ…˜μ΄ μžˆμŠ΅λ‹ˆλ‹€.
@Transactional public class NotificationService { // ... @TransactionalEventListener(condition = "#event.isClosed() == false") public void notifyPollOpen(PollEvent event) { // νˆ¬ν‘œ 생성 μ•Œλ¦Ό } @TransactionalEventListener(condition = "#event.isClosed() == true") public void notifyPollClosed(PollEvent event) { // νˆ¬ν‘œ μ’…λ£Œ μ•Œλ¦Ό } }
Java
볡사
이처럼 condition 으둜 둜직 μ‹€ν–‰ 쑰건을 μ„€μ •ν•˜λ©΄ λ©”μ„œλ“œ λ‹¨μœ„λ‘œ λ‘œμ§μ„ λ‚˜λˆŒ 수 μžˆμ–΄ μ•žμ„œ μ–ΈκΈ‰ν•œ μœ μ§€/보수 λ¬Έμ œμ μ„ ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. condition 을 ν™•μΈν•˜λŠ” μ‹œμ μ€, 컀밋 이후 listener(ApplicationListenerMethodAdapter) 의 processEvent κ°€ ν˜ΈμΆœλ˜λŠ”λ°, ν•΄λ‹Ή λ©”μ„œλ“œ λ‚΄λΆ€μ—μ„œ condition 을 κΊΌλ‚΄μ–΄ ν™•μΈν•©λ‹ˆλ‹€(ν•΄λ‹Ή λ‘œμ§μ„ μˆ˜ν–‰ν•˜λŠ” μ†ŒμŠ€ μ½”λ“œ).
또 ν•œκ°€μ§€ μž₯점이 μžˆλŠ”λ°, λ§Œμ•½ condition 쑰건 없이 λ‹¨μˆœνžˆ λ©”μ„œλ“œλ§Œ λ‚˜λˆ„κ³  ν•΄λ‹Ή λ©”μ„œλ“œ λ‚΄μ—μ„œ λΆ„κΈ°λ¬ΈμœΌλ‘œ μ²˜λ¦¬ν•œλ‹€λ©΄ λ‹€μŒκ³Ό 같이 ꡬ성할 수 μžˆμŠ΅λ‹ˆλ‹€.
@Transactional public class NotificationService { // ... @TransactionalEventListener public void notifyPollOpen(PollEvent event) { if (!event.isClosed()) { // νˆ¬ν‘œ 생성 μ•Œλ¦Ό } } @TransactionalEventListener public void notifyPollClosed(PollEvent event) { if (event.isClosed()) { // νˆ¬ν‘œ μ’…λ£Œ μ•Œλ¦Ό } } }
Java
볡사
이 μƒνƒœμ—μ„œλŠ” λ“±λ‘λ˜λŠ” listener κ°€ 두 개(notifyPollOpen, notifyPollClosed)κ°€ 되고 두 λ©”μ„œλ“œ λͺ¨λ‘ 싀행이 되고 if ꡬ문으둜 μ•Œλ¦Ό λ‘œμ§μ„ μ²˜λ¦¬ν• μ§€ 말지 ν™•μΈν•©λ‹ˆλ‹€. μ΄λ ‡κ²Œ λœλ‹€λ©΄ 두 λ©”μ„œλ“œ λͺ¨λ‘ νŠΈλžœμž­μ…˜μ„ μƒˆλ‘œ μ–»κ²Œ λ©λ‹ˆλ‹€. 즉 νˆ¬ν‘œ 생성 μ‹œμ— νˆ¬ν‘œ 생성 μ•Œλ¦Ό 둜직 의 νŠΈλžœμž­μ…˜κ³Ό νˆ¬ν‘œ μ’…λ£Œ μ•Œλ¦Ό 둜직 의 νŠΈλžœμž­μ…˜μ΄ λͺ¨λ‘ μ—΄λ¦¬κ²Œ λ˜μ–΄ λΆˆν•„μš”ν•œ νŠΈλžœμž­μ…˜μ„ μ–»μ–΄ λΉ„νš¨μœ¨μ μž…λ‹ˆλ‹€. ν•˜μ§€λ§Œ condition 으둜 처리λ₯Ό ν•œλ‹€λ©΄, condition 을 ν™•μΈν•˜λŠ” λ‘œμ§μ€ μƒˆλ‘œμš΄ μ“°λ ˆλ“œλ₯Ό μ—΄μ–΄ μ•Œλ¦Ό λ‘œμ§μ„ μ‹€ν–‰ν•˜κΈ° μ „μž…λ‹ˆλ‹€. 이에 따라 λΆˆν•„μš”ν•œ νŠΈλžœμž­μ…˜μ„ μƒμ„±ν•˜μ§€ μ•Šμ„ 수 μžˆμŠ΅λ‹ˆλ‹€.
β€’
phase
κ΅¬λ…ν•˜λŠ” 이벀트 둜직이 μ–Έμ œ 처리될 지 μ •ν•  수 μžˆλŠ” μ˜΅μ…˜μž…λ‹ˆλ‹€. 선택할 수 μžˆλŠ” phase λ‘œλŠ” BEFORE_COMMIT, BEFORE_COMPLETION, AFTER_COMMIT, AFTER_COMPLETION λ“± 총 4개의 phase κ°€ μžˆμŠ΅λ‹ˆλ‹€. *_COMMIT 은 메인 둜직이 컀밋이 λ˜μ—ˆμ„ λ•Œ, 즉 status 에 둀백이 λ°œμƒν•˜μ§€ μ•Šμ•˜μ„ λ•Œ λ™μž‘ν•˜κ³ , *_COMPLETION 은 컀밋/둀백에 상관없이 λ™μž‘ν•©λ‹ˆλ‹€. 기본값은 AFTER_COMMIT 이고, 이에 따라 메인 둜직인 νˆ¬ν‘œ 생성 둜직이 λ‘€λ°±λ˜μ§€ μ•Šκ³  μ˜¨μ „νžˆ μ»€λ°‹λœ 이후에 μ•Œλ¦Ό 둜직이 λ™μž‘ν•˜κ²Œ λ©λ‹ˆλ‹€.

ꡬ독 λ™μž‘ 원리

AbstractPlatformTransactionManager 의 processCommit λ©”μ„œλ“œλŠ” ν•΄λ‹Ή μ“°λ ˆλ“œμ˜ νŠΈλžœμž­μ…˜ 컀밋 λ‘œμ§μ„ μˆ˜ν–‰ν•©λ‹ˆλ‹€. 이 λ•Œ μ»€λ°‹ν•˜κΈ° μ „κ³Ό 후에 μˆ˜ν–‰λ˜μ–΄μ•Ό ν•  λ‘œμ§λ“€μ„ μˆ˜ν–‰ν•˜λŠ”λ° phase 의 섀정에 따라 이벀트 λ‘œμ§λ„ 컀밋 μ „/후에 μˆ˜ν–‰λ©λ‹ˆλ‹€. 이 ν¬μŠ€νŒ…μ—μ„œλŠ” 컀밋 μ΄ν›„μ˜ 이벀트 둜직 μˆ˜ν–‰μ— λŒ€ν•΄ μ•Œμ•„λ³΄κ² μŠ΅λ‹ˆλ‹€.
processCommit λ©”μ„œλ“œ λ‚΄μ—μ„œ 컀밋 λ‘œμ§μ„ μˆ˜ν–‰ν•˜κ³ λ‚œ λ’€, λ°œν–‰ν•  λ•Œ λ“±λ‘λœ TransactionSynchronization 듀을 κ°€μ Έμ˜¨ λ’€(μ†ŒμŠ€ μ½”λ“œ), TransactionSynchronizationUtils μ—μ„œ κ°€μ Έμ˜¨ synch 듀을 λ°˜λ³΅λ¬Έμ„ λŒλ©΄μ„œ 이벀트 둜직이 μˆ˜ν–‰λ©λ‹ˆλ‹€(μ†ŒμŠ€ μ½”λ“œ).
μ—¬κΈ°μ„œ μ£Όμš”ν•˜κ²Œ λ‹€λ£¨μ–΄μ§€λŠ” TransactionSynchronization κ°μ²΄λŠ” νŠΈλžœμž­μ…˜ 컀밋 전후에 μž‘μ—…μ„ μ‹€ν–‰μ‹œν‚€κΈ° μœ„ν•œ μΈν„°νŽ˜μ΄μŠ€μž…λ‹ˆλ‹€. μ•žμ„œ phase 둜 μ μš©ν•  수 μžˆλŠ” beforeCommit, beforeCompletion, afterCommit, afterCompletion 넀가지 λ©”μ„œλ“œλ₯Ό κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€. κ΅¬ν˜„μ²΄μ— 따라 처리 방법이 λ‹€λ₯΄λ©°, @TransactionalEventListener λŠ”Β TransactionalApplicationListenerSynchronizationΒ λΌλŠ” κ΅¬ν˜„μ²΄λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. ν•΄λ‹Ή κ΅¬ν˜„μ²΄μ—λŠ” listener(ꡬ독 λ©”μ„œλ“œ)와 인자둜 받은 이벀트 객체가 있고, ꡬ독 λ©”μ„œλ“œλ₯Ό μ‹€ν–‰ν•¨μœΌλ‘œμ¨ ꡬ독 둜직이 λ™μž‘ν•˜κ²Œ λ©λ‹ˆλ‹€.

AfterCompletion이 μ™œ κ±°κΈ°μ„œ λ‚˜μ™€β€¦?

μ†ŒμŠ€ μ½”λ“œλ₯Ό μ‚΄νŽ΄λ³΄λ©° κ°€μž₯ κΆκΈˆν–ˆλ˜ 뢀뢄은 triggerAfterCompletion μ—μ„œ μ‹€ν–‰λœλ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€.
μœ„μ˜ μ†ŒμŠ€ μ½”λ“œλ₯Ό λ‹€μ‹œ ν•œλ²ˆ λ³΄κ² μŠ΅λ‹ˆλ‹€.
public abstract class TransactionSynchronizationUtils { public static void invokeAfterCommit(@Nullable List<TransactionSynchronization> synchronizations) { if (synchronizations != null) { for (TransactionSynchronization synchronization : synchronizations) { synchronization.afterCommit(); } } } public static void invokeAfterCompletion(@Nullable List<TransactionSynchronization> synchronizations, int completionStatus) { if (synchronizations != null) { for (TransactionSynchronization synchronization : synchronizations) { try { synchronization.afterCompletion(completionStatus); } catch (Throwable ex) { logger.debug("TransactionSynchronization.afterCompletion threw exception", ex); } } } } }
Java
볡사
@TransactionalEventListener phase μ˜΅μ…˜μœΌλ‘œ 기본값인 after commit 으둜 μ„€μ •ν•˜κ³ , triggerAfterCommit λ©”μ„œλ“œλ„ μ‘΄μž¬ν•˜λŠ”λ° μ™œ after completion 으둜 싀행이 λ˜μ—ˆμ„κΉŒμš”?
사싀 trggerCommit μ—μ„œλŠ” 아무것도 ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. ν•΄λ‹Ή λ©”μ„œλ“œλŠ” κ΅¬ν˜„λ˜μ–΄μžˆμ§€ μ•Šκ³  μΈν„°νŽ˜μ΄μŠ€μ˜ λ””ν΄νŠΈ λ©”μ„œλ“œλŠ” λΉ„μ–΄μžˆλŠ” μƒνƒœμž…λ‹ˆλ‹€. κ·Έλž˜μ„œ 사싀 triggerAfterCommit κ³Ό triggerAfterCompletion 둜직 λ‘˜ λ‹€ νƒ€λŠ”λ°, μ‹€μ œ 싀행은 triggerAfterCompletion μ—μ„œλ§Œ μ§„ν–‰λ˜λŠ” κ²ƒμž…λ‹ˆλ‹€.
afterCommit κ³Ό afterCompletion 을 λ‘˜λ‘œ λ‚˜λˆˆ 이유λ₯Ό μ•Œμ•„λ³΄κΈ° μœ„ν•΄ TransactionSynchronization μΈν„°νŽ˜μ΄μŠ€μ˜ 주석을 μ½μ–΄λ³΄μ•˜λŠ”λ°μš”, 기본적인 μ„€λͺ…은 λ©”μ„œλ“œλͺ…μ—μ„œ 잘 λ“œλŸ¬λ‚©λ‹ˆλ‹€. afterCommit 은 컀밋 이 μ§„ν–‰λœ 이후, afterCompletion 은 컀밋/λ‘€λ°± 상관없이 진행 λ©λ‹ˆλ‹€.
그런데 사싀 ν˜„μž¬ 둜직이 μ§„ν–‰λ˜κ³  μžˆλŠ” μ‹œμ μ€ 이미 메인 둜직의 νŠΈλžœμž­μ…˜ 둜직이 컀밋이 된 이후 μž…λ‹ˆλ‹€. 즉 λ‘€λ°± μƒνƒœκ°€ 있기 νž˜λ“  μƒν™©μž…λ‹ˆλ‹€. κ·Έλ ‡λ‹€λ©΄ λ‘˜μ˜ μ‹€μ§ˆμ μΈ μ°¨μ΄λŠ” λ¬΄μ—‡μΌκΉŒμš”?
# afterCommit Throws: RuntimeException – in case of errors; will be propagated to the caller (note: do not throw TransactionException subclasses here!) # afterCompletion Throws: RuntimeException – in case of errors; will be logged but not propagated (note: do not throw TransactionException subclasses here!)
Plain Text
볡사
μ£Όμ„μ˜ Throws 뢀뢄을 보면, afterCommit 은 μ˜ˆμ™Έκ°€ 호좜된 λ©”μ„œλ“œμ— μ „νŒŒκ°€ 되고, afterCompletion 은 μ „νŒŒκ°€ λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 이에 따라 afterCompletion 은 κ΅¬ν˜„μ²΄ λ‘œμ§μ—μ„œ try/catch 둜 감싸져 있고 μ˜ˆμ™Έμ— λŒ€ν•΄ DEBUG 둜만 λ‘œκΉ…ν•˜κ²Œ λ©λ‹ˆλ‹€.
ν•˜μ§€λ§Œ 저희 μ„œλΉ„μŠ€ νŠΉμ„±μƒ μ•Œλ¦Ό λ‘œμ§μ—μ„œ μ˜ˆμ™Έκ°€ λ°œμƒν–ˆμ„ μ‹œ, DEBUG 레벨 λŒ€μ‹  WARN 레벨 μ΄μƒμ˜ λ‘œκΉ…μ„ ν•΄μ•Ό ν•˜κ³ , 이에 λŒ€ν•΄ ν•΄κ²°μ±…μœΌλ‘œλŠ” afterCommit 을 직접 κ΅¬ν˜„ν•˜κ±°λ‚˜ μŠ¬λž™ μ•Œλ¦Ό λ‘œμ§μ—μ„œ try/catch 둜 μ˜ˆμ™Έλ₯Ό 핸듀링할 μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. 이 해결책듀도 μ’‹μ§€λ§Œ μ•„λ¬΄λž˜λ„ 각각 μž₯단점이 λšœλ ·ν•΄ λ³΄μž…λ‹ˆλ‹€. κ·Έλž˜μ„œ μ•žμ„  ν•΄κ²°μ±…λ“€ λŒ€μ‹  비동기λ₯Ό μ μš©ν•˜μ—¬ μ˜ˆμ™Έ μ²˜λ¦¬μ— λŒ€ν•œ 핸듀링이 κ°€λŠ₯ν•˜λ„λ‘ κ΅¬μ„±ν•΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

비동기 μ„€μ •

이벀트 λ°œν–‰/ꡬ독 λ°©μ‹μœΌλ‘œ λ³€κ²½ν•˜λ©΄μ„œ μ˜μ‘΄μ„±μ„ λ–Όμ–΄ λ‚΄κ³ , β€˜νˆ¬ν‘œ μƒμ„±β€™μ΄λΌλŠ” 메인 둜직과 β€˜μŠ¬λž™ μ•Œλ¦Όβ€™ λΆ€κ°€ 둜직의 νŠΈλžœμž­μ…˜μ„ λ‚˜λˆ„μ–΄ μ•Œλ¦Ό λ‘œμ§μ—μ„œ 둀백이 λ˜λ”λΌλ„ νˆ¬ν‘œ 생성은 컀밋이 λ˜λ„λ‘ ν•˜μ˜€κ³ , νˆ¬ν‘œ 생성 둜직의 컀밋이 잘 μ΄λ£¨μ–΄μ‘Œμ„λ•Œλ§Œ μ•Œλ¦Ό 둜직이 λ™μž‘ν•˜λ„λ‘ λ§Œλ“€μ–΄ λ³΄μ•˜μŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ 아직 문제점이 남아 μžˆλŠ”λ°μš”, ν˜„μž¬ λ™μž‘ν•˜λŠ” 이벀트 λ°©μ‹μ˜ μ•Œλ¦Ό λ‘œμ§μ€ λ™κΈ°μ μœΌλ‘œ μ‹€ν–‰λ˜μ–΄ κ²°κ΅­ μ•Œλ¦Ό 둜직이 λͺ¨λ‘ μ™„λ£Œν•  λ•ŒκΉŒμ§€ νˆ¬ν‘œ 생성 API λŠ” λŒ€κΈ°ν•  μˆ˜λ°–μ— μ—†κ³  μ΄λŠ” 응닡 latency λ₯Ό 길게 λ§Œλ“€κ²Œ λ©λ‹ˆλ‹€. μ£Ό 관심사도 μ•„λ‹Œ λ‘œμ§μ„ ꡳ이 기닀리지 μ•Šκ³  ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ νˆ¬ν‘œ 생성에 λŒ€ν•œ 응닡을 λŒλ €μ£ΌλŠ” 것이 더 μžμ—°μŠ€λŸ½κΈ°μ—, 비동기가 ν•„μš”ν•œ 상황이라고 νŒλ‹¨ν•˜μ—¬ μ μš©ν–ˆμŠ΅λ‹ˆλ‹€.

Async μ“°λ ˆλ“œ ν’€ μ μš©ν•˜κΈ°

비동기 이벀트 μ²˜λ¦¬λŠ” λ³„λ„μ˜ μ“°λ ˆλ“œμ—μ„œ λ™μž‘ν•©λ‹ˆλ‹€. 이 λ•Œ, 이벀트 μ²˜λ¦¬λ§ˆλ‹€ λ¬΄ν•œνžˆ μ“°λ ˆλ“œλ₯Ό μƒμ„±ν•˜κΈ° λ³΄λ‹€λŠ” μ“°λ ˆλ“œ 풀을 μ‚¬μš©ν•˜μ—¬ κ΄€λ¦¬ν•˜λŠ” 방법을 선택할 수 μžˆμŠ΅λ‹ˆλ‹€.
@EnableAsync @Configuration public class AsyncConfig implements AsyncConfigurer { private static final int THREAD_COUNT = 4; @Override public Executor getAsyncExecutor() { return Executors.newFixedThreadPool(THREAD_COUNT); } }
Java
볡사
ν•˜μ§€λ§Œ λΉ„λ™κΈ°λ‘œ μ•Œλ¦Ό λ‘œμ§μ„ μˆ˜ν–‰ν•˜λ©΄ ControllerAdvice 에 μž‘νžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€. μ²˜μŒμ—λŠ” μ™œ μž‘νžˆμ§€ μ•Šμ„κΉŒ 이해할 수 μ—†μ—ˆλŠ”λ°, μƒκ°ν•΄λ³΄λ‹ˆ μž‘νžˆμ§€ μ•ŠλŠ” 것이 λ‹Ήμ—°ν•©λ‹ˆλ‹€. μš”μ²­μ— λŒ€ν•œ 응닡은 DispatcherServlet 의 mvc λ°©μ‹μœΌλ‘œ μˆ˜ν–‰μ΄ 되고 핸듀링 μ‹œμ— try catch 둜 감싸 μ˜ˆμ™Έκ°€ λ°œμƒν•˜λ©΄ 이λ₯Ό μž‘μ•„ ControllerAdvice μ—μ„œ 처리λ₯Ό ν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ λΉ„λ™κΈ°λŠ” μƒˆλ‘œμš΄ μ“°λ ˆλ“œλ₯Ό μƒμ„±ν•˜μ—¬ 둜직이 μˆ˜ν–‰λ˜κΈ° λ•Œλ¬Έμ— DispatcherServlet μ—μ„œ μˆ˜ν–‰λœ 둜직이 μ•„λ‹ˆκ³ , 이에 따라 μ˜ˆμ™Έκ°€ 작힐 수 μ—†λŠ” κ΅¬μ‘°μž…λ‹ˆλ‹€. 이에 따라 λΉ„λ™κΈ°λ‘œ μˆ˜ν–‰λ˜λŠ” λ‘œμ§μ—μ„œ λ°œμƒν•˜λŠ” μ˜ˆμ™ΈλŠ” λ‹€λ₯Έ λ°©μ‹μœΌλ‘œ 핸듀링해야 ν•˜κ³  기본적으둜 try/catch ꡬ문으둜 처리λ₯Ό ν•  수 μžˆλŠ”λ°, 이λ₯Ό μ „μ—­μ μœΌλ‘œ λ„μ™€μ£ΌλŠ” ν΄λž˜μŠ€κ°€ μžˆμŠ΅λ‹ˆλ‹€.
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(AsyncExceptionHandler.class); @Override public void handleUncaughtException(Throwable ex, Method method, Object... params) { logger.warn("비동기 μ²˜λ¦¬μ€‘ μ˜ˆμ™Έκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€\n" + "μ˜ˆμ™Έ 메세지 : " + ex.getMessage() + "\n" + "λ©”μ†Œλ“œ : " + method.getName() + "\n" + "νŒŒλΌλ―Έν„° : " + Arrays.toString(params)); }
Java
볡사
@EnableAsync @Configuration public class AsyncConfig implements AsyncConfigurer { // ... @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new AsyncExceptionHandler(); } }
Java
볡사
AsyncUncaughtExceptionHandler λ₯Ό 상속받아 비동기 μ“°λ ˆλ“œμ—μ„œ λ°œμƒν•˜λŠ” μ˜ˆμ™Έλ₯Ό μ „μ—­μ μœΌλ‘œ μ²˜λ¦¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

κ°œμ„ μ 

이벀트 λ°©μ‹μœΌλ‘œ μ „ν™˜ν•˜κΈ° μ „μ˜ λ¬Έμ œμ μ„ λͺ¨λ‘ ν•΄κ²°ν•΄λ³΄μ•˜λŠ”λ°μš”, 아직 μ˜ˆμ™Έ μ²˜λ¦¬μ— λŒ€ν•œ λ¬Έμ œκ°€ 남아 μžˆμŠ΅λ‹ˆλ‹€. ν˜„μž¬λŠ” μ•Œλ¦Ό 둜직의 μ˜ˆμ™Έμ— λŒ€ν•΄ 둜그둜만 좜λ ₯ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό ν•΄κ²°ν•  수 μžˆλŠ” λ°©λ²•μœΌλ‘œλŠ”, μ•Œλ¦Όμ„ λ°œμ†‘ν•  λ•Œ λ°°μΉ˜μ„± λ‘œμ§μ„ μΆ”κ°€ν•˜μ—¬ 아직 μ•Œλ¦Όμ„ 보내지 μ•Šμ€, 즉 μ•žμ„œ νˆ¬ν‘œ μƒμ„±μ‹œ μ˜ˆμ™Έλ‘œ 인해 μ•Œλ¦Όμ„ 보내지 λͺ»ν•œ λ ˆμ½”λ“œμ— λŒ€ν•΄ 좔가적인 λ‘œμ§μ„ μ‹€ν–‰ν•˜λ„λ‘ ν•˜λŠ” 방법이 μžˆμŠ΅λ‹ˆλ‹€.
public void notifyPollOpen(PollEvent event) { // findAllWhereNotnotified(); // μ„±κ³΅ν•˜λ©΄ λͺ¨λ‘ notified = true μ—…λ°μ΄νŠΈ }
Java
볡사
μ˜ˆμ™Έλ‘œ 인해 μ•Œλ¦Όμ„ 보내지 λͺ»ν–ˆλ˜ νˆ¬ν‘œλ“€μ— λŒ€ν•΄ μž¬μ²˜λ¦¬ν•˜λŠ” λ‘œμ§μ€ μˆ˜ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ 이 방법도 κ²°κ΅­μ—λŠ” μ‹€μ‹œκ°„μ„±μ΄ λΆ€μ‘±ν•˜κ³ , μ˜ˆμ™Έ 상황에 λŒ€ν•œ 좔적과 즉각적인 λŒ€μ‘μ„ ν•  수 μ—†λŠ” λ°©λ²•μž…λ‹ˆλ‹€. 결ꡭ은 MSA 와 같이 μ„œλΉ„μŠ€λ₯Ό μ•„μ˜ˆ λ‚˜λˆ„μ–΄ μ•Œλ¦Όμ— λŒ€ν•œ μƒˆλ‘œμš΄ API λ₯Ό κ΅¬μ„±ν•˜κ³ , μ•Œλ¦Όμ— λŒ€ν•΄ μš”μ²­μ„ 보낼 수 μžˆλ„λ‘ ν•˜μ—¬ 이에 λŒ€ν•œ 응닡을 λ”°λ‘œ κ΄€λ¦¬ν•˜λŠ” 것이 쒋은 방법이 될 κ²ƒμž…λ‹ˆλ‹€.