Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions chap10/최인준.md
Original file line number Diff line number Diff line change
@@ -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 사용하기

<br>

**로컬 핸들러 비동기 실행**

스프링의 @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 활용

→ 트랜잭션이 성공할 때만 이벤트 핸들러가 실행됨

→ 이제 이벤트 처리 실패만 고민하면 됨