Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Week 9] 이벤트 - 박준수 #50

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
89 changes: 88 additions & 1 deletion chap08/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,88 @@
# 8. 애그리거트 트랜잭션 관리
# 8. 애그리거트 트랜잭션 관리

### 8.1 애그리거트와 트랜잭션

- 트랜잭션 처리 방식

1. 선점 방식 (비관적 락) 2. 비선점 방식 (낙관적 락)

### 8.2 선점 잠금

- 한 트랜잭션에서 쓰기를 하는 동안 다른 트랜잭션에서는 잠금으로 대기
- JPA에서는 EntityManager는 LockModeType을 인자로 받는 find()메서드르 제공함

```java
entityManger.find(Order.class,LockModeType.PESSIMISTIC_WRITE)
```

- for update 쿼리를 이용해서 선점 잠금을 구현한다.

```java
@Lock(LockModeType.PESSIMISTIC_WRITE)
```

- 스프링 데이터 JPA는 @Lock 애너테이션을 사용해서 잠금 모드를 지정한다.

**교착상태**

- 선점 잠금에서 교착생태가 발생할 수 있음
- 스레드 1, 2에서 각각 자원을 선점한 상태에서 서로 상대가 가진 자원에 대해 선점 잠금을 시도할 경우 발생함
- 해결 방법
1. 잠금을 구할 때 최대 대기 시간을 지정해야 함
2. 각 잠금 순서를 일치시켜주면 데드락이 발생하지 않음

```java
// JPA 해결 방법
Map<String, Object> hints=new HashMap();
hits.put("javax.peristence.lock.timeout",2000);
Order order=entityManager.find(Order.class,LockModeType.PESSIMISTIC_WRITE)
```

- DBMS에 따라 힌트가 적용되지 않을 수가 있다.

```java
//스프링 데이터 JPA는 @QueryHints 애너테이션을 사용해서 쿼리 힌트를 작성할 수 있다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints({
@QueryHint(name = "javax.peristence.lock.timeout", 2000)
}
)
@Query("selet m from Member m where m.id = :id)
Optional<Member>findByIdForUpdate(@Param("id) MemberId memberId)
```

### 비선점 잠금

- 동시에 접근하는 것을 막는 대신 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식이다.

```java
UPDATE aggtable SET version=version+1,colx=?,coly=?
WHERE aggid=?and version=현재 버전
```

- JPA는 버전을 이용한 비선점 잠금 기능을 지원한다. @Version 애너테이션을 붙이고 매핑되는 테이블에 버전을 저장할 칼럼을 추가하면 된다.

```java
public class Order {

@Version
private long version;

...
}
```

- 비선점 잠금 충돌 발생시 스프링 프레임워크에서 OptimisticLockingFailureException이 발생함

**강제 버전 증가**

- 루트 애그리거트의 값이 바뀌지 않았더라다도 애그리거트의 구성요소 중 일부 값이 바뀌면 논리적으로 그 애그리거트는 바뀐 것이다.
- 따라서 애그리거트 내의 어떤 구성요소의 상태가 바뀌면 루트 애그리거트이 버전 값이 증가해야 비선점 잠금이 올바르게 동작한다.
- JPA의 경우 LockModeType.OPTIMISTIC_FORCE_INCREMENT를 사용하면 해당 엔티티의 상태가 변경되엇는지에 상관엇이 트랜잭션 종료 시점에 버전 값 증가
처리를 한다.
- 스프링 데이터 JPA에서는 @Lock 애너테이션을 이용해서 지정하면 된다.

### 오프라인 선점 잠금

- 잠금 선점 시도, 잠금 확인, 잠금 해제, 잠금 유효시간 연장의 네 가지 기능이 필요하다
![img.png](img.png)
1 change: 0 additions & 1 deletion chap09/README.md

This file was deleted.

1 change: 0 additions & 1 deletion chap10/README.md

This file was deleted.

100 changes: 100 additions & 0 deletions chap10/박준수.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# 10. 이벤트

### 10.1 시스템 간 강결합 문제

**주문 서비스 + 결제 서비스(외부)**

1. 외부 서비스가 정상이 아닐 경우 트랜잭션 처리를 어떻게 해야할까?
2. 성능에 대한 문제 → 외부 서비스 성능에 직접적인 영향을 받게 됨

→ 주문 바운디드 컨텍스트와 결제 바운디드 컨텍스트 간의 강결합 때문

→ **해결 방법 : 이벤트를 사용하는 것**

### 10.2 이벤트 개요

- 이벤트 관련 구성요소
- 이벤트 생성 주체
- 이벤트 디스패처(이벤트 퍼블리셔)
- 이벤트 핸들러(이벤트 구독자)
- 이벤트 용도
- 트리거 : 도메인 상태가 바뀔 때 다른 후처리가 필요하면 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있음
- 동기화 : 배송지를 변경하면 외부 배송 서비스에 바뀐 배송지 정보를 전송해야 함

→ 이벤트를 사용하여 주문 도메인에서 결제(환불) 도메인으로의 의존을 제거함

### 10.3 이벤트, 핸들러, 디스패처 구현

이벤트 처리 흐름

- 응용 서비스 - 도메인 - Events - ApplicationEventPublisher - 이벤트 핸들러
1. 도메인 기능을 실행한다.
2. 도메인 기능은 Events.raise()를 이용해서 이벤트를 발생시킨다.
3. Events.raise()는 스프링이 제공하는 ApplicationEventPublisher를 이용해서 이벤트를 출판한다.
4. ApplicationEventPublisher는 @EventLister(이벤트타입.class) 애너테이션이 붙은 메서드를 찾아 실행한다.

### 10.4 동기 이벤트 처리

- 외부 환불 서비스 실행에 실패하였다고 해서 반드시 트랜잭션을 롤백해야 할까?

외부 시스템과의 연동을 동기로 처리할 때 발생하는 성능과 트랜잭션 범위 문제를 해소하는 방법은 이벤트를 비동기로 처리하거나 이벤트와 트랜잭션을 연계하는 것이다.

### 10.5 비동기 이벤트 처리

- ‘A 하면 이어서 B 하라’ → ‘A 하면 최대 언제까지 B 하라’

→ B를 하는데 실패하면 일정 간격으로 재시도를 하거나 수동 처리를 해도 상관 없는 경우가 있다.

‘→ A 하면 최대 언제까지 B 하라’ = 이벤트를 비동기로 구현하면 됨

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

- @EnableAsync 애너테이선을 사용해서 비동기 기능을 활성화한다.
- 이벤트 핸들러 메서드에 @Ansync 어노테이션을 붙임

**메시징 시스템을 이용한 비동기 구현**

- Kafka, RabbitMQ
- 필요하다면 이벤트를 발생시키는 도메인 기능과 메시지 큐에 이벤트를 저장하는 절차를 한 트랜잭션으로 묶어야 한다.
- 도메인 기능을 실행한 결과를 DB에 반영하고 이 과정에서 발생한 이벤트를 메시지 큐에 저장하는 것을 같은 트랜잭션 범위에서 실행하려면 **글로벌 트랜잭션**이 필요함
- 글로벌 트랜잭션을 사용하면 안전하게 이벤트를 메시지 큐에 전달할 수 있는 장점이 있지만 반대로 글로벌 트랜잭션으로 인해 전체 성능이 떨어지는 단점도 있음, 글로벌 트랜잭션을 지원하지 않는 메시징 시스템도 있다.

**이벤트 저장소를 이용한 비동기 처리**

1. **포워더 방식 - 스케줄러 사용**
- 이벤트가 발생하면 핸들러는 스토리지에 이벤트를 저장한다.
- 포워더는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러를 실행한다.
- 포워더는 별도 스레드를 이용하기 때문에 이벤트 발행과 처리가 비동기로 처리된다.
1. **API 방식 - REST API 요청**
- 외부 핸들러가 API 서버를 통해 이벤트 목록을 가져간다.
- 포워더 방식은 이벤트를 어디까지 처리했는지 추적하는 역할이 포워더에 있다면 API 방식에서는 이벤트 목록을 요구하는 외부 핸들러가 자신이 어디까지 이벤트를 처리했는지 기억해야한다.

### 10.6 이벤트 적용 시 추가 고려 사항

1. 이벤트 소스를 EventEntry에 추가할지 여부
- EventEntry는 이벤트 발생 주체에 대한 정보를 갖지 않는다.
2. 포워더에서 전송 실패를 얼마나 허용할 것이냐에 대한 것
- 포워더는 이벤트 전송에 실패하면 실패한 이벤트로부터 다시 읽어와 전송을 시도한다.

→ 이벤트를 전송하는데 3회 실패했다면 해당 이벤트는 생략하고 다음 이벤트로 넘어간다는 등의 정책이 필요하다.

- 처리에 실패한 이벤트를 생략하지 않고 별도 실패용 DB나 메시지 큐에 저장하기도 한다. 처리에 실패한 이벤트를 물리적인 저장소에 남겨두면 이후 실패 이유 분석이나 후처리에 도움이 된다.
3. 이벤트 손실에 대한 것
- 이벤트 저장소를 사용하는 방식은 이벤트 발생과 이벤트 저장을 한 트랜잭션으로 처리하기 때문에 트랜잭션에 성공하면 이벤트가 저장소에 보관된다는 것을 보장할 수 있다.
- 로컬 핸들러를 이용해서 이벤트를 비동기로 처리할 경우 이벤트 처리에 실패하면 이벤트를 유실하게 된다.
4. 이벤트 순서에 대한 것
- 이벤트 발생 순서대로 외부 시스템에 전달해야 할 경우, 이벤트 저장소를 사용하는 것이 좋다.
- 이벤트 저장소는 저장소에 이벤트를 발생 순서대로 저장하고 그 순서대로 이벤트 목록을 제공함
5. 이벤트 재처리에 대한 것
- 동일한 이벤트를 다시 처리해야 할 때 이벤트를 어떻게 할지 결정해야 한다.
- 마지막으로 처리한 이벤트의 순번을 기억해 두었다가 이미 처리한 순번의 이벤트가 도착하면 해당 이벤트를 처리하지 않고 무시하는 것
- 멱등성을 보장하도록 처리함

**이벤트 처리와 DB 트랜잭션 고려**

- 이벤트 처리를 동기로 하든 비동기로 하든 이벤트 처리 실패와 트랜잭션 실패를 함께 고려해야 한다.
- 트랜잭션이 성공할 때만 이벤트 핸들러를 실행하는 것
- @TransactionalEventLister 애너테이션을 지원함
- 스프링은 트랜잭션 커밋에 성공한 뒤에 핸들러 메서드를 실행한다. 중간에 에러가 발생해서 트랜잭션이 롤백되면 핸들러 메서드를 실행하지 않는다.
- 이벤트 핸들러를 실행했는데 트랜잭션이 롤백되는 상황은 발생 하지 않는다.
- 즉, 트랜잭션이 성공할 때만 이벤트 핸들러를 실행하게 되면 트랜잭션 실패에 대한 경우의 수가 줄어 이벤트 처리 실패만 고민하면 됨