이번 글에서 필자는 EDA 즉 Event Driven Architecture를 적용해 보면서 했던 경험들과 고민들을 작성하고자 한다.
해당 글을 작성하기에 앞서 알아야 하는 개념들에 대해 정리 후 본론으로 들어가 보자.
- Event란?
- 비동기 or 동기
- 트랜잭션
Event란?
본글에서 말하는 Event는 도메인 모델의 상태 변화 즉 변화를 일으키는 사건을 의미한다.
예를 들어 상품을 주문한다 가정해 보자.
- 상품 재고를 확인한다.
- 상품 재고를 차감한다.
- 주문을 생성한다.
- 주문 완료 알림을 보낸다.
위와 같은 프로세스를 가진다고 가정한다면 상품이라는 도메인은 '상품 재고가 차감되었다.'라는 이벤트를 발행하고,
주문은 '주문이 생성되었다'라는 이벤트를 발행하게 될 것입니다.
비동기 or 동기
비동기와 동기는 통신 방식입니다.
동기 방식의 통신은 현재 Thread가 요청을 보낸 후 응답을 받을 때까지 대기하는 것이고,
비동기는 현재 Thread가 요청을 보낸 후 응답을 받을 때까지 대기하지 않고, 후속 작업을 콜백이나 이벤트 기반으로 처리하는 것입니다.
즉 작업의 완료 여부를 어떻게 처리하느냐의 차이가 있습니다.
트랜잭션
업무 처리 단위를 의미합니다.
해당 업무는 ACID를 보장해야 하며 이를 보장하는 방법이 트랜잭션입니다.
- Atomic(원자성): 트랜잭션 내 데이터들은 데이터베이스에 모두 반영되거나 전혀 반영되지 않아야 합니다.
- Consistency(일관성): 트랜잭션의 작업 결과는 항상 일관성을 띄워야 합니다.
- Isolation(독립성): 각각의 트랜잭션은 독립적으로 실행되어야 합니다.
- Durability(영구성): 트랜잭션이 성공적으로 끝났을 때 결과는 영구적으로 반영되어야 합니다.
위의 4가지 원칙을 보장하여야 합니다.
- Active(활성): 트랜잭션이 실행 중인 상태
- Parially Committed(부분완료): 트랜잭션 내 연산이 모두 끝났지만 반영되지 않은 상태
- Committed(완료): 트랜잭션이 종료되어 데이터베이스에 반영된 상태
- Failed(실패): 트랜잭션 연산 중 오류가 발생한 상태
- Aborted(철회): 트랜잭션이 비정상 종료되어 Rollback 연산을 수행한 상태
트랜잭션은 위와 같은 5가지 상태를 가지게 됩니다.
특이점으로 Parially Committed에서 Aborted로 가는 경우는 연산은 끝났지만 데이터베이스에 오류가 발생 혹은 종료되어 Commit상태로 가지 못하는 상황 등이 존재합니다.
서비스 간 직접 호출 방식과 Event발행 소비 방식
앞선 예시를 가져와 주문, 상품, 알림 이 3가지 서비스를 예시로 사용하여 두 방식 간의 차이가 무엇인지 알아보겠습니다.
- OrderService: 주문 서비스
- ProductService: 상품 서비스
- NotifyService: 알림 서비스
Event발행 방식을 알기 전 저는 서비스 간 호출을 진행할 때 아래와 같이 코드를 작성하였습니다.
public class OrderUsecase{
private final OrderService orderService;
private final ProductService productService;
private final NotifyService notifyService;
public void order(...) {
// 상품 재고 차감
productService.reduceStock(...);
// 주문 생성
Order order = orderService.createOrder(...);
// 주문 알림 발행
notifyService.sendOrderNotification(order);
}
}
이와 같이 코드를 작성하더라도 문제는 없을 것입니다.
상품재고가 없다면 reduceStock Transaction에서 오류가 발생하여 다음 Process를 진행하지 않을 것이고,
주문이 생성되지 않는다면 알림이 발행되지 않기 때문입니다.
여기서 고민해 볼 사항은 '알림 발행이 실시간성을 보장해야 하고 알림 발행에 실패한 것이 주문 프로세스에 영향을 끼쳐야 하는가?'입니다.
물론 간단하게 처리하려면 notifyService의 sendOrderNotification메소드에 @Async 어노테이션을 붙이면 간단하게 비동기로 처리할 수 있을 것입니다.
하지만 여기서 고민해 볼 부분은 만약 이렇게 개발을 계속하게 된다면 어떤 불편함이 존재할까요?
NotifyService는 알림을 발행하는 여러 서비스에서 불리게 될 것이며 종속성을 띄게 될 것입니다.
그럼 우리는 이 NotifyService의 알림 발행 기능을 수정하기 위해서는 여러 서비스를 돌아다니며 수정해야 하고, 이는 기능이 추가되면 추가될수록 관리가 힘들어진다는 불편함을 낳을 것입니다.
또한 알림을 발행하다 실패하거나 서버가 다운되어 버리면 알림이 발행되지 않는 상황이 발생할 수 있습니다.
그럼 어떻게 해야 이 불편함과 예외상황을 대응할 수 있을까요?
서버가 의도치 않게 다운되는 현상을 JVM에 Graceful Shutdown옵션을 주면서 어느 정도 해결할 수 있습니다.
다만, 이는 서버를 종료할 때 내부 작동 중인 작업을 모두 수행한 후 종료되는 방식으로 중간에 오류가 터졌다거나 아니면 컴퓨터가 나갔다거나 하는 상황은 대응할 수 없습니다. (ex: OOM, 하드웨어 장애, kill -9)
이런 상황을 대응하기 위해서는 몇 가지 방법을 생각해 볼 수 있습니다.
1. DB에 발행해야 하는 알림을 저장
첫 번째로 DB에 발행해야 하는 알림을 저장하고, 스케줄링을 통해 n개의 알림을 발행하는 방법입니다.
문자 단체발송이나 카카오톡 단체 메시지 발송의 경우 이와 같은 방법을 많이 사용하는 것을 봤는데요.
이는 스케줄링을 통해 n개의 메시지만 처리하기 때문에 메시지 발행 서버의 부하가 일정하다는 장점이 존재합니다.
다만 너무 많은 알림이 저장되게 된다면, 메시지 발생시간이 지연되게 되는 단점이 존재합니다.
또한, 발행한 메시지에 대해 flag값을 변경해 주거나 데이터를 삭제해주어야 하기 때문에 DB부하가 발생할 수 있습니다.
2. Event발행
문제점 중 첫 번째 NotifyService의 호출 서비스가 많아 NotifyService의 관리가 힘들다!
이 부분은 Event를 발행하여 소비하는 방법으로 변경하면서 간편하게 변경할 수 있습니다.
- 주문 생성
- OrderCreated Event발행
- NotifyEventHandler에서 OrderCreated Event 소비
- NotifyService 알림 발행
Event를 발행한다면 Order와 Notiry 간의 결합이 느슨한 결합이 되어 유연성과 확장성이 좋은 형태로 변경할 수 있습니다.
그럼 서버가 다운되어 버린다면 알림은 서버가 다시 살아났을 때 잘 발행할 수 있을까요?
이벤트 스트리밍 플랫폼(kafka, net, rabbitmq 등)을 사용한다면 서버가 다운되더라도 정상적인 알림 발행이 가능합니다.
왜 가능할까요?
우선 이벤트 스트리밍 플랫폼은 따로 Instance를 필요로 합니다.
즉 이벤트 관리 서버가 존재한다는 의미이죠.
이벤트가 발생하면 Message를 만들어 발행하 Queue에 담아 보관을 하게 됩니다.
즉, 메인 서비스 서버가 죽었다 하더라도 이벤트를 관리하는 서버가 죽지 않는다면 얼마든지 이벤트를 소비할 수 있다는 것이죠.
그럼 이벤트 스트리밍 서버 여기서가 다운된다면 메시지는 사라지는 거 아닌가요?
Kafka를 예시로 들면 Kafka는 메시지를 Log Segment로 저장하므로, Kafka가 다운된다 하더라도 문제가 없습니다.
다만 옵션에 따라 오래된 Log는 삭제하니 이에 문제가 발생할 수 있습니다.
하지만 이벤트 스트리밍 플랫폼은 이벤트에 대해 고가용성을 보장하는 만큼 어느 정도 신뢰를 가질 수 있습니다.
그럼 문제가 다 해결될까요?
우리가 위에 해결하고자 하는 불편함과 문제 상황 대응은 이렇게 충분한 것 같습니다.
Kafka와 같은 이벤트 스트리밍 플랫폼을 사용할 때 발생할 수 있는 문제들이 존재하지만,
이는 아래 글에서 다루도록 하겠습니다.
EDA가 뭔가요?
지금까지는 Event를 발행하면 어떤 문제점 및 문제 상황을 대응할 수 있을지 알아보았습니다.
하지만 결국 Event Driven Architecture가 무엇인지는 아직 잘 와닿지는 않네요.
Martin Fowler가 EDA 개념을 정리하면서 나오게 되었는데요,
EDA는 쉽게 Event를 발행하고 수신자가 Event를 소비하는 형태의 시스템 아키텍처입니다.
EDA는 크게 3가지 구성요소를 가지게 됩니다.
- Event generator: 표준화된 형식의 이벤트를 생성
- Event Producer: 이벤트를 필요로 하는 시스템까지 발송
- Event Consumer: 이벤트를 구독하고 처리
이렇게 Event를 사용하여 통신을 하게 되었을 때 장점은 무엇이 있을 까요?
- 비동기 처리에 용이
- Loose coupling
등이 존재한다 생각합니다.
단점은 어떤 점들이 존재할까요?
- 트랜잭션 관리의 어려움
- 디버깅의 어려움
- Event관리의 어려움
이러한 장단점이 존재하므로 EDA를 적용한다면 현재 서비스에 어떤 문제를 해결하기 위해 적용하는지가 명확해야 합니다.
그럼 비동기 개발을 하려면 그냥 EDA적용하면 되는 건가요?
아닙니다. 앞서 말했듯이 무작정 EDA를 적용한다면 Event를 받아 발행하는 Message관리도 어려울 것이고,
관심사가 명확하지 않다면 오히려 복잡한 시스템으로 탄생하게 될 것입니다.
그럼 언제 적용하는 것이 좋을까요?
서버 간 결합을 낮춰야 할 때, 여러 작업이 비동기로 처리되어야 할 때와 같이 어떤 문제를 해결하고자 적용하는 것이 적절하다 생각합니다.
실제로 EDA를 적용하여 개발하였지만, 오히려 Event들을 처리하는 Process의 복잡도에 의해 리펙토링 하는 상황도 있었고,
실시간성을 보장해야 하지만 Event를 사용하여 비동기로 처리하게 되면서 실시간성을 훼손시키는 상황도 발생하였습니다.
즉 EDA든 Event든 만능치료제가 아니니 꼭 알고 사용하기를 권장드립니다.
References
https://medium.com/dtevangelist/event-driven-microservice-%EB%9E%80-54b4eaf7cc4a
https://jaehun2841.github.io/2019/06/23/2019-06-23-event-driven-architecture/
'Server' 카테고리의 다른 글
[Monorepo] 모노레포 그것이 답인가? (1) | 2025.05.22 |
---|---|
[해결 과제] 인증 서비스 왜 Refactoring 하였을까? (0) | 2025.04.04 |
[MSA] 왜 MSA로 가야하나요? (0) | 2025.04.03 |
[Architecture] Clean Architecture VS Hexagonal Architecture (0) | 2025.01.14 |
[DNS] 너의 주소는? (6) | 2024.12.30 |