From b2a7f39ca7b0ef49077ed20def292c052d43fbb2 Mon Sep 17 00:00:00 2001 From: dlswns2480 Date: Tue, 13 Aug 2024 14:05:42 +0900 Subject: [PATCH] =?UTF-8?q?docs=20:=2010=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\354\265\234\354\235\270\354\244\200.md" | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 "chap10/\354\265\234\354\235\270\354\244\200.md" diff --git "a/chap10/\354\265\234\354\235\270\354\244\200.md" "b/chap10/\354\265\234\354\235\270\354\244\200.md" new file mode 100644 index 0000000..528844b --- /dev/null +++ "b/chap10/\354\265\234\354\235\270\354\244\200.md" @@ -0,0 +1,161 @@ +## 시스템 간 강결합 문제 + +예시 상황 : 쇼핑몰에서 구매를 취소하면 환불을 처리해야 하는 상황 + +- 환불 기능을 실행하는 주체는 주문 도메인 +- 주문 도메인이 환불 기능을 제공하는 도메인 서비스를 파라미터로 전달받음 + +→ 환불을 처리하는 외부 서비스가 정상이 아닐 경우 트랜잭션 처리가 애매해짐 + +→ 환불을 처리하는 외부 서비스가 성능이 안좋을 시 직접적인 영향을 받음 + +→ 기능이 추가시에 여로 로직이 섞이게 되는 문제 발생 + +**→ 시스템 간 강결합으로 인한 문제!** + +이 문제를 이벤트를 사용하여 해결할 수 있다. + +## 이벤트 개요 + +**이벤트(event)란?** + +‘과거에 벌어진 어떤 것’을 의미 + +**이벤트 관련 구성 요소** + +- 이벤트 생성 주체 : 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체 +- 이벤트 디스패처(퍼블리셔) : 이벤트 생성 주체와 이벤트 핸들러를 연결 +- 이벤트 핸들러(구독자) : 이벤트를 전달받아 이벤트에 담긴 데이터를 이용해서 원하는 기능 실행 + +**이벤트의 구성** + +- 이벤트 종류 : 클래스 이름으로 이벤트 종류를 표현 +- 이벤트 발생 시간 +- 추가 데이터 : 주문번호, 신규 배송지 정보 등 이벤트와 관련된 정보 + +**이벤트 용도** + +- 트리거 용도 + - 도메인의 상태가 바뀔 때 다른 후처리를 실행하기 위한 트리거로 이벤트 사용가능 +- 시스템 간의 데이터 동기화 + +**이벤트 장점** + +이벤트를 사용하면 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다. + +기능 확장에도 용이한데 구매 취소 시 환불과 함께 이메일로 취소 내용을 보내는 기능이 추가되었다고 하면 이메일 발송을 처리하는 이벤트 핸들러를 추가 구현하면 된다. + +## 이벤트, 핸들러, 디스패처 구현 + +- 이벤트 클래스 : 이벤트를 표현한다. +- 디스패처 : 스프링이 제공하는 ApplicationEventPublisher를 이용한다. +- Events : 이벤트를 발행한다. 이벤트 발행을 위해 ApplicationEventPublisher를 사용한다. +- 이벤트 핸들러 : 이벤트를 수신해서 처리한다. 스프링이 제공하는 기능을 사용한다. + +**이벤트 클래스** + +단순히 원하는 클래스를 이벤트로 사용하면 된다. + +- 네이밍 시 과거 시제를 사용한다. +- 이벤트를 처리하는데 필요한 최소한의 데이터를 포함해야 한다. + +**Event 클래스와 ApplicationEventPublisher** + +이벤트 발생과 출판을 위해 스프링이 제공하는 ApplicationEventPublisher를 사용한다. + +Events 클래스를 통해 호출할 수 있다. (raise() 메서드 활용) + +**이벤트 발생과 이벤트 핸들러** + +- Events.raise() 를 활용해 이벤트를 발생시킨다. +- 이벤트를 처리할 핸들러는 @EventListener 애너테이션을 통해 구현한다. + +**흐름 정리** + +1. 도메인 기능을 실행한다. +2. 도메인 기능은 Events.raise()를 이용해서 이벤트를 발생시킨다. +3. Events.raise()는 스프링이 제공하는 ApplicationEventPublisher를 이용해서 이벤트를 출판한다. +4. ApplicationEventPublisher는 @EventListener 애너테이션이 붙은 메서드를 찾아 실행한다. + +→ 도메인 상태 변경과 이벤트 핸들러는 같은 트랜잭션 범위에서 실행된다. + +## 동기 이벤트 처리 문제 + +이벤트를 사용해서 강결합 문제는 해소했지만 외부 서비스에 영향을 받는 문제가 남아있다. + +외부 서비스의 성능에 직접적인 영향을 받는 문제는 어떻게 해결해야 할까? + +외부 서비스 실행에 실패했다고 반드시 트랜잭션을 롤백해야할까? + +이런 애매한 문제를 해소하는 방법은 이벤트를 비동기로 처리하거나 이벤트와 트랜잭션을 연계하는 것이다. + +## 비동기 이벤트 처리 + +요구사항 중에 ‘A하면 최대 언제까지 B하라’ 라는 요구사항이 있다면 이는 **비동기 실행** 즉, 별도 스레드로 B를 수행하는 핸들러를 실행하는 방식으로 구현할 수 있다. + +크게 네 가지 방식이 있다. + +- 로컬 핸들러를 비동기로 실행하기 +- 메시지 큐를 사용하기 +- 이벤트 저장소와 이벤트 포워더 사용하기 +- 이벤트 저장소와 이벤트 제공 API 사용하기 + +
+ +**로컬 핸들러 비동기 실행** + +스프링의 @Async 애너테이션을 통해 쉽게 구현할 수 있다. + +- @EnableAsync 애너테이션을 통해 비동기 기능을 활성화 한다. +- 이벤트 핸들러 메서드에 @Async 애너테이션을 붙인다. + +**메시징 시스템을 이용한 비동기 구현** + +- 카프카나 래빗MQ와 같은 메시징 시스템 활용 가능 +- 이벤트를 메시지큐에 보냄 → 메시지 큐는 이벤트를 메시지 리스너에 전달 → 리스너는 알맞은 이벤트 핸들러를 이용해서 이벤트 처리 +- 이벤트를 발생시키는 기능, 메시지 큐에 이벤트를 저장하는 절차를 한 트랜잭션으로 묶는게 조음 + +**이벤트 저장소를 이용한 비동기 처리(포워더)** + +- Transactional Outbox 패턴 +- 이벤트를 DB에 저장한 뒤 포워더를 통해 주기적으로 이벤트 핸들러에 전달 +- 포워더가 이벤트를 어디까지 전달했는 지 저장해야함(last offset) +- 도메인 상태와 이벤트 저장소로 동일한 DB사용 + +**이벤트 저장소를 이용한 비동기 처리(API)** + +- 외부에 API를 제공하는 방식 +- 외부 핸들러가 API서버를 통해 이벤트 목록을 가져가는 방식 +- 나머진 포워더와 동일함 + +**이벤트 저장소 구현** + +- EventEntry : 저장소에 보관할 이벤트 데이터, 식별자를 가짐 +- EventStroe : 이벤트를 저장하고 조회하는 인터페이스를 제공 +- JdbcEventStore : JDBC를 이용한 EventStore 구현 클래스 +- EventApi : REST API를 이용해서 이벤트 목록을 제공하는 컨트롤러 + +## 이벤트 적용 시 추가 고려사항 + +1. 이벤트 소스를 EventEntry에 추가할 지 → 특정 주체가 발생시킨 이벤트만 조회하려면 추가 해야함 +2. 포워더에서 전송 실패를 얼마나 허용할 것인 지 → 재전송 제한 횟수를 두어야 한다. + 1. 처리에 실패한 이벤트를 저장해두면 실패 이유 분석이나 후처리에 도움이 된다. +3. 이벤트 손실에 대한 것 + 1. 로컬 핸들러를 이용해서 이벤트를 비동기로 처리하면 이벤트 처리에 실패하면 이벤트를 유실하게 된다. + 2. 유실이 큰 문제가 되는 이벤트라면 다른 방식으로 이벤트 구현을 고려해야한다. +4. 이벤트 순서에 대한 것 → 순서가 중요하면 이벤트 저장소를 활용해 구현해야함 +5. 이벤트 재처리에 대한 것 → 마지막으로 처리한 이벤트의 순번을 기억해 두기 + +**이벤트 처리와 DB 트랜잭션 고려** + +이벤트 처리를 동기로 하든 비동기로 하든 이벤트 처리 실패와 트랜잭션 실패를 함께 고려해야 한다. + +모두 고려하면 복잡해지므로 경우의 수를 줄이면 도움이 된다. + +→ 트랜잭션이 성공할 때만 이벤트 핸들러를 실행하자. + +→ @TransactionalEventListener 활용 + +→ 트랜잭션이 성공할 때만 이벤트 핸들러가 실행됨 + +→ 이제 이벤트 처리 실패만 고민하면 됨 \ No newline at end of file