From d5c50bf0bb81e745f7ce083e54e1407048d29219 Mon Sep 17 00:00:00 2001 From: GaBaljaintheroom Date: Wed, 14 Aug 2024 14:43:31 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20main=20=EB=A8=B8=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chap08/README.md | 89 +++++++++++++++++++++++++++++++++++++++++++++++- chap09/README.md | 1 - chap10/README.md | 1 - 3 files changed, 88 insertions(+), 3 deletions(-) delete mode 100644 chap09/README.md delete mode 100644 chap10/README.md diff --git a/chap08/README.md b/chap08/README.md index 3843a87..f86120a 100644 --- a/chap08/README.md +++ b/chap08/README.md @@ -1 +1,88 @@ -# 8. 애그리거트 트랜잭션 관리 \ No newline at end of file +# 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 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) + OptionalfindByIdForUpdate(@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) \ No newline at end of file diff --git a/chap09/README.md b/chap09/README.md deleted file mode 100644 index 78747ab..0000000 --- a/chap09/README.md +++ /dev/null @@ -1 +0,0 @@ -# 9. 도메인 모델과 바운디드 컨텍스트 \ No newline at end of file diff --git a/chap10/README.md b/chap10/README.md deleted file mode 100644 index c08b9ac..0000000 --- a/chap10/README.md +++ /dev/null @@ -1 +0,0 @@ -# 10. 이벤트 \ No newline at end of file From 7c4a29ba37448649fdc021a32035e9f40f58ca49 Mon Sep 17 00:00:00 2001 From: GaBaljaintheroom Date: Wed, 14 Aug 2024 14:44:30 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[Week=209]=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?-=20=EB=B0=95=EC=A4=80=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\353\260\225\354\244\200\354\210\230.md" | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 "chap10/\353\260\225\354\244\200\354\210\230.md" diff --git "a/chap10/\353\260\225\354\244\200\354\210\230.md" "b/chap10/\353\260\225\354\244\200\354\210\230.md" new file mode 100644 index 0000000..5ff697d --- /dev/null +++ "b/chap10/\353\260\225\354\244\200\354\210\230.md" @@ -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 애너테이션을 지원함 + - 스프링은 트랜잭션 커밋에 성공한 뒤에 핸들러 메서드를 실행한다. 중간에 에러가 발생해서 트랜잭션이 롤백되면 핸들러 메서드를 실행하지 않는다. + - 이벤트 핸들러를 실행했는데 트랜잭션이 롤백되는 상황은 발생 하지 않는다. + - 즉, 트랜잭션이 성공할 때만 이벤트 핸들러를 실행하게 되면 트랜잭션 실패에 대한 경우의 수가 줄어 이벤트 처리 실패만 고민하면 됨 \ No newline at end of file