목표
- 이벤트 주도 설계를 이용하지 않았을 때의 문제점에 대해 알아볼 것 입니다.
- 이벤트 주도 설계를 이용한 뒤 유지보수성 향상, 도메인 로직의 명확성 향상, 어플리케이션의 성능과, 클라이언트의 경험을 향상 시킬 것 입니다.
- 서비스 확장이 편리한지, 확인합니다.
주의사항
- 모듈간의 이벤트를 주고받기 위해 사용된 RabbitMq는 Docker를 이용해 설치했으며, 이 글에서 자세히 다루지 않습니다.
- 이벤트주도 설계에 매우 정확한 지식을 가지고 설계한 것은 아니며, 이벤트로 모듈간 통신을하면 이런, 장점들이 있구나를 알아보기 위한 모의 시뮬레이션정도로 봐주시면 감사할것 같습니다. (제가 잘못된 지식을 가지고 있는 경우, 필히 댓글로 알려주시면 정말 감사드리겠습니다! 추가적으로 정확한 지식은 알아가고 있습니다~)
어플리케이션 시나리오
- 사용자가 주문을 취소합니다.
- 주문 취소 시 환불이 진행되어야 합니다. 환불은 외부 결제모듈을 통해 이루어집니다.
고려사항
- 주문취소 후 환불
모듈에서 장애
가 있는 경우, 주문트랜잭션을 어떻게 처리할 것
인가. - 주문 로직과,
관련이 없는 환불로직이 섞이지
않는가. - 환불 모듈의 응답이 지연되는 경우,
사용자의 경험
이 저해되지 않는가. - 사용자에게 환급이 완료되었다는
이메일을 보내는 추가 기능
이 도입된 경우 어떻게 처리할 것인가.
구현
1. 이벤트 주도 설계 도입전
https://github.com/galid1/before_event_driven_example
우선, 구현해 볼 상황은, 이벤트 주도 설계 이전의 상황입니다. 코드
는 위 주소에 업로드 해놓았습니다.
1.1 이벤트 주도 설계 도입 전 아키텍처
주문 취소 요청시 한 서버내에서 모든 처리가 이루어지는데요. 이때, 필요한 서비스를 직접 호출하며, 의존성을 가지게 되는 구조입니다.
- 사용자는 주문 취소 요청을 합니다.
- 주문 취소 서비스가 실행되며 주문 취소 서비스에서 환불처리 서비스를 호출합니다.
- 모든 처리가 완료되면 사용자에게 응답을 합니다.
1.2 도메인 모델
OrderEntity
- 주문 도메인엔티티로 주문 취소만을 표현하기 위해,
orderId(PK), orderer(주문자), orderStatus(주문상태)
세개의 필드만을 두었습니다.
OrderRefundEntity
- 주문 취소시, 환불을 위해 생성되는 엔티티입니다.
orderRefundId(PK), orderId(주문번호), refundStatus(주문상태)
를 통해 어떤 주문에 대한 환불이 어떠한 상태(환불 완료, 환불 중)에 있는지 표현합니다.
1.3 중요 코드
Rest API 컨트롤러로써, 주문 생성과, 주문 취소 기능이 있습니다.
사용자가 주문을 취소하면 위의 cancelOrder()
가 동작합니다. 주문 취소 로직이 동작한 후, 환불을 위해 환불서비스에 환불 요청을 합니다.
환불 서비스에서는, 환불 내역을 생성하고, 환불 모듈에 환불을 요청합니다.
환불 모듈에서는, 환불 처리를 시뮬레이션하기 위해, 스레드를 5초간 잠재우고, 장애상황을 시뮬레이션하기 위해 고의적으로 예외를 던집니다.
1.4 테스트
Postman을 이용해 1번 주문 취소요청을 하면, 위와 같이 환급 모듈이 처리되는 동안 클라이언트는 대기 상태
가 됩니다.
또한, 환급 외부 모듈에 장애가 생겨, 사용자의 주문 취소 요청과, 환불 요청이 모두 롤백되었습니다. 따라서 주문 취소처리가 되지 않고, 환불 요청에 대한 기록이 없음을 볼 수 있습니다.
1.5 정리 및 문제점
시스템 강결합 :
- 주문 도메인과 관련이 적은 환불 도메인 로직이 섞여, 환불 도메인에 변경사항이 발생하여도, 주문 도메인 서비스의 수정이 필요합니다.
- 이메일로 환불 결과를 알리는등 추가기능이 도입되면 주문 도메인이 결합되는 모듈이 증가합니다.
서버 성능과 사용자 경험 저하 :
- 사용자는 얼마나 걸릴지 모를 환불 모듈의 처리를 기다려야 합니다.
트랜잭션 처리 :
- 환불 모듈에 장애가 발생한 경우
주문 취소까지 롤백
됩니다.
2. 이벤트 주도 설계 도입 및 이메일 알림 기능 확장
https://github.com/galid1/after_event_driven_example
코드는 위 링크에서 볼 수 있습니다.
2.1 이벤트 주도 설계 도입 후 아키텍처
이제 서비스를 별도의 서버로 나누고 각각의 디비를 별도로 구성하고, 메시지 큐를통해 이벤트를 전달받는 구조로 변환합니다.
- 사용자가 주문 취소 요청을 합니다.
- 주문서비스에서 주문취소 후, refund.queue로 이벤트를 발행한 뒤, 사용자에게 바로 응답을 합니다.
- 환불서비스는 refund.queue의 이벤트를 가져와 처리한 뒤, mail.queue에 이벤트를 발행합니다.
- 메일서비스에서 사용자에게 메일을 발송합니다.
2.2 중요사항
- 고객이 주문취소 요청시, 환불 모듈에 장애가 생기더라도, 고객의 주문취소 요청은 받아들이고, 환불 처리는
나중에 해도 됩니다
. 즉, 사실 강한 결합을 가질 필요가 없는 서비스들이었습니다. ⇒ 이점을 이용해 서비스를 분리하고, 이벤트 기반 비동기 처리를 할 수 있습니다. - 또한 주문 도메인에서는 주문 취소 이벤트만 발행하면 되므로, 주문 취소시 추가 기능은 이벤트를 통해 코드 수정 없이 확장이 가능해집니다. → 시스템 강결합 해결, 클라이언트 경험 향상, 서버 성능 향상
2.3 변경사항
- 주문서비스와 환불서비스가 분리되었습니다.
- 주문서비스에서 주문 로직과 관련없는 환불 서비스를 직접 호출하지 않아 의존성을 가지지 않습니다.
- 또한, 주문서비스, 환불 서비스의 변경 없이 메일서비스가 추가 가능해졌습니다.
2.4 변경 후 테스트
마찬가지로 PostMan으로 요청을 합니다. 1, 2, 3
번 주문을 한번에 주문취소 요청합니다. ⇒ 주문서비스가, 환불서비스에 의존하지 않고 단순히 이벤트만 발행하기 때문에, 이벤트주도 설계 이전처럼, 클라이언트가 대기하는 현상이 발생하지 않습니다.
주문 취소 처리가, 환불 모듈의 실행 여부와 관계없이 성공적으로 이루어졌음을 확인할 수 있습니다. 또한, ORDER_REFUND
테이블이 별도로 나뉜것을 볼 수 있습니다.
환불서비스에서, 메시지큐에 등록된 테스크를 가져와 환불서비스를 시작합니다. 비동기로 처리하기 때문에, 환불 처리의 응답을 기다리지 않고 스레드 풀 만큼 동시 처리를 진행합니다.
메일 발송은 단순히 콘솔에 출력하는것으로 대신하였습니다.
환불 요청관련 데이터가 DB에 생성되었음을 확인할 수 있습니다.
2.5 정리
각각의 서비스를 별도의 서버로 나누고, 이벤트를 통해 서비스간 통신을 한 결과 아래와 같은 이점이 발생했습니다.
주문서비스에서 환불처리를 위해서, 로직을 직접 작성하지 않고 이벤트만을 발행하기 때문에 상관없는 로직이 혼재하지 않습니다.
주문 취소후, 환불서비스에 의존하지 않기 때문에, 장애가 발생하더라도 사용자의 주문취소 요청은 정상처리가 됩니다.
환불서비스는 메시지큐를 통해 환불요청을 받으므로,
꼭, 한번
은 환불처리를 수행하게 됩니다.만약 주문 취소 후, 이메일 발송처럼 추가적인 행동이 필요하다면, 해당 메시지큐를 구독하는 서비스만을 추가함으로써 기존 코드를 수정하지 않을 수 있습니다.
성능이 부족한 경우 각각의 서비스 별로 스케일아웃이 가능하며, 별도의 디비를 사용함으로써, 상황에 맞는 디비(RDB or NoSQL)를 선택할 수 있습니다.