diff --git "a/chap10/\352\271\200\354\236\254\354\227\260.md" "b/chap10/\352\271\200\354\236\254\354\227\260.md" new file mode 100644 index 0000000..42aa2a5 --- /dev/null +++ "b/chap10/\352\271\200\354\236\254\354\227\260.md" @@ -0,0 +1,315 @@ +# 이벤트 + +## 시스템 간 강결합 문제 + +환불 기능을 실행한다고 했을 때, 도메인 서비스를 입력받을 수 있다. +다음과 같이 말이다. + +![](https://i.imgur.com/fFaw2Eo.png) + +최근들어서 내가 되게 많이 쓰고 있는 방식과 굉장히 비슷하다. + +또한 다음과 같이 응용서비스에서 실행 해줄 수도 있다. + +![](https://i.imgur.com/wzPaC0N.png) + +쨌든, 위의 과정 중에서 환불 시스템은 외부의 시스템이니 언제나 터질 수 있다. 이렇게 외부 시스템과 연결된 경우, 항상 하게 되는 고민이 동일한 트랜잭션으로 묶을 것이냐는 것인데, 둘 다 나름의 이유가 존재한다. + +트랜잭션을 롤백하는 경우는, 당연히 환불에 실패했으니, 주문 취소 행위자체도 롤백해야 하는 것이 아니냐. 라는 의견으로 좁혀질 수도 있고, 그와 반대로, 주문 취소만 일단 진행해놓고 환불은 나중에 진행할 수도 있는 것이다. + +또 고민할 수 있는 부분은 성능에 대한 부분이다. +외부 시스템을 사용하는 경우, 성능도 외부 시스템에 의존적으로 변하게 된다. + +이 두가지 문제 이외에 이와 같이 도메인 객체에 서비스를 전달하면, 서로 다른 로직인, 주문 로직과 결제 로직이 섞인다는 문제점도 존재한다. (설계상 문제) + +![](https://i.imgur.com/zG8Ffyy.png) + +이렇게 되면, 환불 기능이 바뀌게 되면 Order도 영향을 받게 된다는 것이고, 객체 지향의 장점 중 하나인, 다른 객체로 변경사항이 최소로 퍼져나가는 것을 어기게 된다. + +만일 여기서 환불에 대한 결과를 통지해야한다면, 더 많은 Service가 필요할 것이고, 이로써 더 복잡해지고 설계상의 허점은 많아질 것이다. + +![](https://i.imgur.com/7TryVWJ.png) + +이러한 문제가 발생하는 이유가 무엇일까? 이는, 바운디드 컨텍스트 간의 강결합 때문에 발생하는 것이다. + +이벤트를 사용해 이 강결합을 없애보자. + +## 이벤트 개요 + +이벤트는 발생하고 끝나지 않는다. 이벤트가 발생하게 되면, 그 이후 동작들도 이어지게 된다. + +### 이벤트 관련 구성요소 + +사실 엄청 중요한 내용인지는 모르겠지만, 이벤트 관련 구성요소는 다음과 같다. + +![](https://i.imgur.com/ehass6b.png) + +당연히, 퍼블리셔가 있고, 구독자가 있는 형태이다. (퍼블리싱 하면 받아서 처리하는 형식) + +이벤트를 생성하면 이벤트 디스패처가 이를 받아, 알맞은 이벤트 핸들러를 찾아 처리하게 된다. +이 때, 구현 방식에 따라 이벤트 생성과 처리를 동기나 비동기로도 처리할 수 있다. + +### 이벤트의 구성 + +이벤트는 발생한 이벤트에 대한 다음과 같은 정보를 담는다. + +![](https://i.imgur.com/7cfXT0M.png) + +그리고 처음 알았는데, 아무래도 이벤트는 바로 직전이라고 하더라도, 현재 일어난 것이 아니니, 과거시제를 사용해준다고 한다. + +![](https://i.imgur.com/mZNsA5b.png) + +나왔다. 도메인 이벤트.. + +진짜 매력적이다. 도메인에서 ShippingInfo가 변경되었다고 이벤트를 발행하면 어디에선가 이를 받아서 처리하겠지? (Events.raise라는 이벤트 발행을 도와주는 Static 메서드도 제공하나보다, 진짜 엘레강스하다.) + +이벤트는 꼭 필요한 정보들을 다 담고 있어야 한다고 한다. 당연하지만, 그렇지 않다면 비효율적인 처리를 해야하니까. (DB 접근 등등) + +최근 들어, 이벤트에 너무 많은 정보를 담아, 효율적이긴 하지만, 이벤트에 이렇게 많은 정보를 담아도 되는 것일까? 라고 생각했던 나의 의구심을 확신으로 만들어주는 챕터이다. (물론, 너무 필요없는 정보를 담으면 좋지 않다.) + +### 이벤트 용도 + +이벤트가 트리거로 사용될 때는 다음과 같이 사용된다. + +![](https://i.imgur.com/8hvBuCk.png) + +또한, 시스템 간의 데이터 동기화를 이루어낼 때도 사용할 수 있다. +다만, 이렇게 좋은 이벤트도, 뭔가 이 이벤트를 발생시켰을 때, 어디로 가서 로직을 처리하는지 등을 트래킹 하는 것은 조금 힘들었던 부분이 있었던 것 같다. 조금 단점? + +### 이벤트 장점 + +이벤트의 장점으로는, 다음과 같다. 강결합을 끊어냈다. + +![](https://i.imgur.com/jaRL3cc.png) + +그렇기에, 기능에 변경이 생기더라도, Order는 계속해서 이벤트를 발행하기만 하면 되기 때문에, 변화되는 사항들이 없다. + +다음과 같이 이루어지기 때문이다. + +![](https://i.imgur.com/XOM8LxT.png) + +## 이벤트, 핸들러, 디스패처 구현 + +이번장은 실제 이벤트와 관련된 코드를 구현해본다고 한다. 뭔가 트랜잭션처럼 프록시를 앞에다가 두고, 처리할 것 같다라는 생각을 하긴 했는데, 막상 실제 구현을 해보려 하니 살짝 설레이기도한다. + +![](https://i.imgur.com/W67djUl.png) + +### 이벤트 클래스 + +이벤트에 사용될 클래스는 딱히 상위 클래스가 존재하지 않는다. 그냥 어떤 객체를 생성하고, 그거를 Publisher가 캐치하기만 하면 된다. + +다만, 네이밍만 신경쓰면된다. + +![](https://i.imgur.com/Dlrla1U.png) + +상위 클래스는 없지만, 공통된 데이터를 하나로 묶기 위해서 상속구조를 사용할 수 있다. + +### Events 클래스와 ApplicationEventPublisher + +![](https://i.imgur.com/vX0nR6e.png) + +뭐야, Events가 Spring혹은 외부에서 제공하는 라이브러리인줄 알았는데, 임의로 구현하신건가.. 조금 짜치는데 + +다만, 주변에서 이벤트를 자주 사용하는 사람들이 많아서, 본 결과 AbstractAggregateRoot를 상속받아, register() 메서드를 활용해 이벤트를 퍼블리싱하는 것 같다. + +![](https://i.imgur.com/qZzyovU.png) + +쨌든, 위와 같이 Events를 선언해놓고, Bean을 초기화해보자 + +### 이벤트 발생과 이벤트 핸들러 + +또 Event를 Listen 하기 위해서는 다음과 같이 어노테이션을 붙여주어야한다. + +![](https://i.imgur.com/hJF7q3W.png) + +### 흐름 정리 + +지금까지의 흐름을 보면 다음과 같다. + +![](https://i.imgur.com/udjpYRx.png) + +코드 흐름을 보면 응용 서비스와 동일한 트랜잭션 범위에서 이벤트 핸들러를 실행하고 있다. + +즉, Event 를 Publishing하면서 되게 동떨어져있다는 느낌을 받기는 하지만, 하나의 트랜잭션으로 이어진다는 말이다. (물론 Propagation 설정에 따라 달라지겠지) + +## 동기 이벤트 처리 문제 + +강결합 문제는 해결했다. 하지만, 트랜잭션과 관련한 것, 그리고 성능과 관련된 문제를 아직 해결하지 못했다. + +## 비동기 이벤트 처리 + +비동기 처리는 이제 `A 하면 B 하라`를 `A 하면 최대 언제까지 B하라`로 치환이 되는 문제에서 활용할 수 있다. + +그러면 이제 비동기로 구현할 수 있는 방법을 알아봐야하지 않겠는가, 비동기로 처리할 수 있는 방법은 다음과 같이 4가지가 있다고 한다. + +![](https://i.imgur.com/IEPfW8e.png) + +당연히 방식에 따라 각자 구현하는 방식도 다르고 그에 따른 장단점이 존재한다. 차례대로 알아보자. + +### 로컬 핸들러 비동기 실행 + +굉장히 쉬운 방법이다. 이벤트 핸들러를 별도 스레도 실행하게 하는 것인데, 이 때 스프링이 제공하는 @Async 어노테이션을 활용하면 해당 방법으로 진행할 수 있다. + +@EnableAsync 어노테이션을 활용해서, 스프링의 비동기 실행 기능을 활성화하도록 하자. + +![](https://i.imgur.com/5QPNqrG.png) + +그러고서 이제 EventListener에다가 Async를 붙여주면 된다. + +### 메시징 시스템을 이용한 비동기 구현 + +비동기로 이벤트를 처리할 때, 가장 많이 사용하는 카프카(Kafka), 래빗MQ(RabbitMQ)와 같은 메시징 시스템을 사용하는 것이다. + +이벤트가 발생하고, 메시징을 전송하게 되었을 때, 메시징 시스템은 이를 받아 알맞은 이벤트 핸들러를 찾아주는 형식이다. + +![](https://i.imgur.com/tXOvLcf.png) + +굉장히 간단히 나타낸 것 같긴하다. + +서로 다른 트랜잭션으로 이루어지는 것을 볼 수 있고, 이를 글로벌 트랜잭션을 활용하여 하나의 트랜잭션으로 처리할 수 있지만, 이는 전체적인 성능을 떨어뜨리기에, 이를 지원하지 않는 메시징 시스템도 존재한다고 한다. (카프카) + +이런 메시징 시스템을 활용하면, 이벤트 발생, 이벤트 처리 프로세스가 별도의 프로세스가 동작하고, 이는 이벤트 발생 JVM과 이벤트 처리 JVM이 다르다는 것을 의미한다. + +이를 동일 JVM으로 처리할 수 있지만, 시스템을 복잡하게 만들 뿐이다. + +### 이벤트 저장소를 이용한 비동기 처리 + +이벤트를 비동기로 처리하는 방법은 DB를 활용해, 저장한 뒤, 이를 별도의 프로그램이 처리하는 것이다. + +![](https://i.imgur.com/9cIusc8.png) + +당연히 주기적으로 읽어와, 처리하겠지? + +이렇게 되면, 당연히 포워더는 별도의 스레드로 운영되기에 비동기로 처리된다. + +이 방식은 도메인의 상태와 이벤트 저장소로 동일한 DB를 활용하게 된다, 즉 도메인 상태 변화와 이벤트 저장이 로컬 트랜잭션으로 처리된다는 것이다. + +이벤트 핸들러가 처리를 실패하더라도 포워더가 다시 읽어오면 되기 때문에, 상관없다. + +또한, 포워더가 하던 일을 API 방식으로 제공할 수 있다. + +그냥 이벤트를 조회하고 가져가는 주체가 변경된 것이라고 간단하게 생각하면된다. + +![](https://i.imgur.com/tsdmSmv.png) + +둘 다, 이벤트 저장소가 필요하다. 코드 구조는 어떻게 될까? + +![](https://i.imgur.com/ig5iiQn.png) + +![](https://i.imgur.com/fxr6OTL.png) + +구조는 굉장히 간단하다. + +API에서 제공하는 형식이다. DB에 다음과 같이 저장되고, 제공도 이와 같이 진행된다. + +- EventEntry + +![](https://i.imgur.com/rLe2Vri.png) + +![](https://i.imgur.com/fqALpKL.png) + +여러 이벤트 형식이 저장될텐데, 어떻게 다양한 이벤트를 하나의 API로 제공할 수 있지라고 생각할 수 있지만, 이는 직렬화에 숨겨져있다. 객체를 json 형태로 직렬화하기만 하면 String 타입으로 모든 객체를 표현할 수 있다. 그리고 type을 application/json으로 명시해놓기만 하면 다 처리되는 것이다. + +API 인터페이스이다. offset, limit이 있는 것을 보니, 최대한 필요한 이벤트 데이터만을 가져가는 형식을 볼 수 있다. + +- EventStore + +![](https://i.imgur.com/VTnsjx8.png) + +정말 간단하게 JDBC Template을 활용하여, DB에 있는 데이터를 다루는 코드이다. +그냥, jdbc template 을 활용해 EventEntry를 저장하거나, EventEtnry를 반환받는 것을 볼 수 있다. + +- JdbcEventStore + +![](https://i.imgur.com/6hvBln3.png) + +![](https://i.imgur.com/7gMwVTQ.png) + +![](https://i.imgur.com/EKHdjXb.png) + +DDL은 다음과 같다. + +- EventEntry + +![](https://i.imgur.com/Kk4uhPL.png) + +이제 실제로, 발생한 이벤트를 저장하는 EventStoreHandler만 구현하면 준비는 끝이다. + +- EventStoreHandler + +![](https://i.imgur.com/g5rABqt.png) + +API를 제공하기 위한 Controller다. + +- EventApi + +![](https://i.imgur.com/IvwxeN4.png) + +![](https://i.imgur.com/AXIPfPV.png) + +이렇게 구현을 완성했으면, 클라이언트는 일정 간격으로 다음 과정을 실행하면 된다. (수정은 x) + +![](https://i.imgur.com/2e7sMII.png) + +중복으로 이벤트를 처리하지 않기 위해 페이징을 야무지게 처리하고 있다. + +![](https://i.imgur.com/FOQBbs2.png) + +이런 식으로 주기적으로 서비스에서 발생한 이벤트를 전달받아 처리하는 것을 볼 수 있다. (위에서 보였던 lastOffset을 활용한 결과로 중복없이 이벤트를 처리하고 있다.) + +포워더도 한번 봐볼까? (똑같이 lastOffset을 기억해뒀다가, 중복없이 이벤트를 처리할 수 있음) + +- EventForwarder + +![](https://i.imgur.com/xKCyOvY.png) + +![](https://i.imgur.com/1E43vW9.png) + +동일하게 주기적으로 lastOffset부터 이벤트를 읽어와, event를 sent해주고, 마지막에 lastOffset을 save 해준다. (처리한 이벤트가 1개 이상인 경우에만) + +보면 알 수 있지만, offsetStore를 활용하는데, DB 테이블에 저장하거나, 로컬 파일에 보관하는 등, 물리적 저장소에 보관하면 된다. + +EventSender는 send라는 인터페이스를 제공하는데, 이는 외부 메시징 시스템을 활용하여 진행하거나, 원하는 핸들러에 이벤트를 전달한다(이벤트를 전달한다는 의미는 동일) + +또한, 이벤트 처리 중 익셉션이 발생하면, 그대로 전파하여 다음 주기에 getAndSend() 메서드를 실행할 때 재 처리할 수 있도록 한다. + +## 이벤트 적용 시 추가 고려 사항 + +이 장에서 구현하지는 않았는데, 이벤트를 구현할 때 추가로 고려할 점이 있는데, 이벤트의 소스를 저장하는 것이다. (어떤 주체로 인해 이벤트가 발생했는지 알아야하기 때문에) + +또한, 이벤트를 처리하는 최대 재시도 횟수를 지정해줘야 한다. 어떤 이벤트가 자꾸 오류를 일으킨다면, 당연히 이 다음 이벤트들을 처리하기 위해서, 넘어가주어야 한다. + +세 번째는 이벤트 손실이 일어날 수도 있다는 것이다. + +네 번째, 이벤트 처리의 순서가 굉장히 중요한 경우, 이벤트 저장소를 사용하는 것이 좋다. 아무래도 메시징 시스템은 사용하는 기술에 따라 이벤트의 순서가 달라질 수도 있기 때문이다. + +다섯 번째, 이벤트 재처리를 고려해야한다. 동일한 이벤트를 다시 처리해야 할 때 이벤트를 어떻게 할지 결정해야 한다. + +가장 쉬운 방법은 마지막으로 처리한 이벤트의 순번을 기억해 두었다가 이미 처리한 순번의 이벤트가 도착하면 해당 이벤트를 처리하지 않고 무시할 수 있다. + +### 이벤트 처리와 DB 트랜잭션 고려 + +DB 트랜잭션도 고려해야 하는데, 예를 들어 주문 취소와 환불 기능을 다음과 같이 이벤트를 이용해서 구현했다고 가정하면 + +![](https://i.imgur.com/PTjN3ey.png) + +![](https://i.imgur.com/5Sj3rA7.png) + +동기로 처리하게 되면, 다음과 같은 플로우로 진행이 될 것이다. 취소를 진행하고, Event를 던지고, 이것을 처리하는 방향으로 + +근데 만일, DB업데이트 과정 중에 Exception이 나버리면 어떻게 될까? 결제 취소는 되었는데, 실제로 결제가 취소되지 않은 상태로 DB에 데이터가 남아있어, 고객은 꿩먹고 알먹기가 되지 않을까? + +그렇기에, 트랜잭션을 더더욱이 신경써야하고, 이는 비동기로 이벤트를 처리할 때에도 고려해야한다. + +![](https://i.imgur.com/tnsg7Xq.png) + +이게 더 최악이다. DB에 업데이트 되고나서, 주문은 취소가 되었는데, 결제 환불이 되지 않았다? 이것은 고객이 노발대발할 사항이다. + +트랜잭션 처리와, 이벤트 처리 모두를 고민하면 경우의 수가 늘어 머리가 아프다. +그렇기에, 이를 줄이는 방식은 다음과 같이 TransactionalEventListener를 추가해주는 것이다. 원래도 동일한 트랜잭션으로 실행이 되긴 하지만, 이처럼 설정하게 되면, 이벤트 핸들러를 실행시키기 이전의 트랜잭션이 커밋이 된 후에 실행이 된다. + +![](https://i.imgur.com/hAZ59qs.png) + +그렇기에, 외부 인프라 딴과 연결되어 있는 경우, 이를 효과적으로 처리할 수 있는 것이다. + +이번 장은 많은 것들을 배울 수 있었던 것 같다.