API 이벤트 기반 구조 (결합도, 확장성, 디버깅)

API 개발하다 보면 서비스 간 의존성 때문에 골치 아플 때가 많습니다. 한 곳을 수정하면 연결된 다른 부분까지 영향을 받아서 배포할 때마다 긴장하게 되는 경험, 개발자라면 누구나 있으실 겁니다. 저도 그랬습니다. 그래서 이벤트 기반 구조를 도입해봤는데, 확장성은 정말 좋아졌지만 예상 못한 문제도 생기더군요. 이벤트 기반 구조가 정답이라고 말하는 분들도 있는데, 확장성과 디버깅 난이도는 동전의 양면 같았습니다.

서비스 간 결합도를 낮추는 원리

이벤트 기반 구조의 핵심은 서비스 간 직접 호출을 없애는 것입니다. 기존 방식에서는 A 서비스가 B 서비스의 API를 직접 호출했습니다. 이렇게 되면 B 서비스가 변경될 때마다 A 서비스도 함께 수정해야 하는 상황이 발생합니다. 이를 강한 결합(Tight Coupling)이라고 부르는데, 쉽게 말해 두 서비스가 끈으로 단단히 묶여 있는 상태입니다.

이벤트 기반에서는 A 서비스가 특정 동작을 완료하면 "주문 생성됨"이라는 이벤트만 발행합니다. B 서비스는 그 이벤트를 구독하고 있다가 알림을 받으면 자기 할 일을 처리하죠. A는 B가 어떻게 동작하는지 전혀 몰라도 되고, B도 A의 내부 로직을 알 필요가 없습니다. 이 방식으로 주문 시스템을 개편했을 때 각 서비스를 독립적으로 배포할 수 있게 되면서 배포 스트레스가 확실히 줄었습니다.

다만 이벤트를 주고받는 메시지 브로커(Message Broker)를 중간에 두어야 합니다. 카프카(Kafka)나 RabbitMQ 같은 도구들이 이 역할을 맡는데, 이것도 관리 포인트가 하나 더 생긴다는 의미이긴 합니다. 일반적으로 결합도가 낮아진다고 알려져 있지만, 이벤트 스키마 관리를 소홀히 하면 오히려 혼란이 생길 수 있었습니다.

확장성이 실제로 어떻게 향상되는가

확장성(Scalability)이라는 건 시스템이 커질 때 얼마나 유연하게 대응할 수 있느냐를 뜻합니다. 이벤트 기반 구조는 이 부분에서 강점이 명확합니다. 새로운 기능을 추가할 때 기존 서비스를 건드리지 않아도 되기 때문입니다. 예를 들어 주문 생성 이벤트를 발행하는 구조에서 갑자기 "주문 생성 시 포인트 적립" 기능을 추가해야 한다면, 포인트 서비스를 새로 만들어서 주문 생성 이벤트를 구독하게만 하면 끝입니다.

제가 진행했던 프로젝트에서는 주문 처리 시스템을 순차 호출 방식에서 이벤트 기반으로 전환했습니다. 기존에는 주문 생성 → 결제 API 호출 → 재고 API 호출 → 알림 API 호출 순서로 진행됐는데, 하나라도 실패하면 전체가 롤백되거나 멈춰버렸습니다. 이벤트 방식으로 바꾸고 나니 각 서비스가 독립적으로 처리하면서 특정 기능 장애가 전체 시스템을 멈추지 않게 되었습니다.

트래픽이 몰릴 때도 유리합니다. 특정 서비스만 부하가 높으면 그 서비스의 인스턴스만 늘리면 됩니다. 예를 들어 알림 발송 서비스에 부하가 집중되면 알림 서비스만 스케일 아웃(Scale-out)하면 되는 거죠. 마이크로서비스 아키텍처(출처: microservices.io)와 결합하면 이런 장점이 더욱 두드러집니다.

  1. 새로운 기능 추가 시 기존 코드 수정 불필요
  2. 특정 서비스만 독립적으로 확장 가능
  3. 장애 발생 시 격리 효과로 전체 시스템 안정성 향상
  4. 비즈니스 로직 변경에 유연하게 대응

확장성을 높이는 게 목표라면 이벤트 기반 구조는 분명 효과적입니다. 하지만 여기서 끝이 아니라는 게 문제였습니다.

비동기 처리가 만드는 복잡성

이벤트 기반 구조는 본질적으로 비동기(Asynchronous) 방식입니다. 요청을 보내고 즉시 응답을 받는 게 아니라, 이벤트를 발행하고 나중에 각 서비스가 알아서 처리합니다. 이게 확장성에는 유리하지만 시스템 동작을 이해하기는 훨씬 어렵게 만듭니다.

동기 방식에서는 코드를 위에서 아래로 따라가면 실행 흐름을 파악할 수 있습니다. 하지만 비동기에서는 이벤트가 발행된 뒤 언제, 어느 서비스가, 어떤 순서로 처리할지 명확하지 않습니다. 특히 여러 서비스가 같은 이벤트를 구독하고 있으면 실행 순서를 보장하기 어렵습니다. 초기에 이 부분을 간과했다가 결제와 재고 처리 순서가 꼬여서 재고는 차감됐는데 결제가 실패하는 상황을 겪었습니다.

이벤트 처리 실패 시 재시도(Retry) 정책도 복잡합니다. 한 번 실패한 이벤트를 몇 번까지 재시도할 것인지, 재시도 간격은 어떻게 할 것인지, 최종 실패 시 어떻게 처리할 것인지 모두 설계해야 합니다. 동기 방식이었다면 예외 처리 한 번으로 끝날 일이 이벤트 기반에서는 훨씬 정교한 오류 처리 메커니즘을 요구합니다.

이벤트 중복 처리 문제도 있습니다. 네트워크 장애 등으로 같은 이벤트가 두 번 발행될 수 있는데, 이를 멱등성(Idempotency) 있게 처리하지 않으면 주문이 두 번 생성되는 식의 치명적인 버그가 발생합니다. 멱등성이란 같은 작업을 여러 번 수행해도 결과가 동일하게 유지되는 특성을 말합니다. 실무에서는 이벤트마다 고유 ID를 부여하고 이미 처리한 ID는 건너뛰는 방식으로 구현했습니다.

디버깅을 어렵게 만드는 요인들

이벤트 기반 구조에서 가장 힘들었던 건 역시 디버깅이었습니다. 문제가 발생했을 때 어디서부터 추적해야 할지 막막한 경우가 많았습니다. 동기 방식에서는 스택 트레이스(Stack Trace)만 봐도 대략 원인을 파악할 수 있는데, 이벤트 방식에서는 여러 서비스에 흩어진 로그를 일일이 뒤져야 했습니다.

이 문제를 해결하려면 분산 추적(Distributed Tracing) 시스템이 필요합니다. 각 이벤트에 고유한 추적 ID(Trace ID)를 부여하고 모든 서비스가 이 ID를 로그에 남기도록 했습니다. 이렇게 하면 하나의 요청이 여러 서비스를 거치며 생성한 모든 로그를 추적 ID로 묶어서 볼 수 있습니다. Zipkin이나 Jaeger 같은 도구를 쓰면 시각화도 가능합니다(출처: OpenTelemetry).

중앙 집중식 로그 수집도 필수입니다. 각 서비스의 로그를 ELK 스택(Elasticsearch, Logstash, Kibana)이나 Datadog 같은 곳에 모아야 합니다. 로그가 각 서버에 흩어져 있으면 이벤트 흐름을 파악하는 것 자체가 불가능합니다. 솔직히 이런 인프라 구축에 초기 비용과 시간이 상당히 들어갔습니다.

이벤트 기반 구조를 도입하려면 모니터링과 로깅 체계부터 갖춰야 한다고 봅니다. 그냥 이벤트만 날리고 받는 구조로 시작하면 나중에 반드시 후회하게 됩니다. 실제로 운영 가시성 확보가 이벤트 설계만큼이나 중요했습니다.

API 이벤트 기반 구조는 확장성과 디버깅 난이도라는 두 마리 토끼를 동시에 안고 갑니다. 확장성이 필요하고 서비스 독립성을 확보하고 싶다면 분명 좋은 선택이지만, 그만큼 운영 복잡도가 올라간다는 사실도 받아들여야 합니다. 소규모 프로젝트에서 무리하게 도입할 필요는 없고, 시스템 규모와 팀 역량을 냉정하게 판단한 뒤 결정하는 게 맞습니다. 이벤트 기반 구조를 고민 중이라면 먼저 로깅과 추적 체계부터 설계해보시길 권합니다.

댓글

이 블로그의 인기 게시물

HTTP 메서드의 필요성 (GET과 POST, PUT과 DELETE, API 보안)

API 없는 세상의 불편함 (로그인 연동, 서비스 구조, 디지털 인프라)

API 이해하기 (서비스 연결, 시스템 협력, 디지털 구조)