Post

이벤트를 통한 비동기 처리와 오류 사례

이벤트를 통한 비동기 처리와 오류 사례

Intro

이 글에서는 이벤트 처리를 통한 비동기 처리가 왜 필요하고 비동기 처리시에 발생할 수 있는 대표적인 문제 상황을 공유하면서 꼭 지켜야할 원칙에 대해서 알아보고자 글을 작성하였습니다.

코드 응집도와 Transaction의 중요성

소프트웨어 개발에서 코드 응집도는 모듈 내의 요소들이 얼마나 밀접하게 관련되어 있는지를 나타내는 개념입니다. 높은 응집도를 가진 코드는 하나의 모듈이 특정 기능을 수행하는 데 필요한 모든 로직을 포함하고 있어, 코드의 가독성이 높고 유지보수가 용이합니다. 반면, 낮은 응집도를 가진 코드는 기능이 여러 모듈에 흩어져 있어 복잡성이 증가하고 오류가 발생할 가능성이 커집니다.

Transaction은 Database에서 작업의 논리적 단위를 의미합니다. Transaction은 일련의 작업이 모두 성공적으로 완료되거나, 실패 시 전혀 실행되지 않도록 보장합니다. 이를 통해 데이터의 일관성무결성을 유지할 수 있습니다. 예를 들어, 은행 송금 시스템에서 계좌 A에서 돈을 빼고 계좌 B에 돈을 추가하는 두 작업은 하나의 Transaction으로 묶여야 하며, 둘 중 하나라도 실패하면 전체 작업이 롤백되어야 합니다.

코드 확장에 따른 응집도 저하

시스템이 성장하면서 새로운 기능이 추가되면, 기존 코드에 로직을 덧붙이는 경우가 많습니다. 이때 설계가 제대로 이루어지지 않으면 응집도 저하가 발생할 수 있습니다. 예를 들어, 주문 처리 모듈에 결제, 배송, 알림 기능이 모두 섞여 있다면, 한 기능의 수정이 다른 기능에 영향을 미칠 가능성이 높아집니다. 이는 코드의 복잡성을 증가시키고 유지보수를 어렵게 만듭니다.

이를 해결하려면 모듈화디커플링이 필요합니다. 모듈화는 각 기능을 독립적인 모듈로 분리해 책임을 명확히 하고, 디커플링은 모듈 간 의존성을 줄여 한 모듈의 변경이 다른 모듈에 영향을 최소화하도록 설계하는 것을 의미합니다.

이벤트를 통한 비동기 처리 디커플링의 중요성

이벤트 기반 아키텍처는 디커플링을 효과적으로 달성하는 방법 중 하나입니다. 이벤트를 사용하면 Publisher(이벤트를 발행하는 주체)와 Subscriber(이벤트를 구독하는 주체)가 직접 연결되지 않고, 비동기적으로 상호작용할 수 있습니다. 이는 시스템의 유연성과 확장성을 높여줍니다.

예를 들어, 전자상거래 시스템에서 사용자가 주문을 생성하면 주문 생성 후 고객에게 이메일 알림을 보내야 한다고 가정해 봅시다. 주문 생성 로직에서 직접 이메일 발송 로직을 호출하면 두 기능이 강하게 결합되어, 주문 생성이 이메일 발송에 의존하게 됩니다. 만약 이메일 발송 서비스가 다운되면 주문 생성까지 실패할 수 있습니다.

반면, 주문 생성 후 “주문 생성 이벤트”를 발행하고, 별도의 이메일 발송 서비스가 이 이벤트를 구독해 처리한다면, 두 기능은 독립적으로 동작할 수 있습니다. 이는 각 모듈의 응집도를 높이고, 시스템 전체의 안정성과 유지보수성을 개선합니다.

이벤트 발행시점은?

이벤트 기반 아키텍처에서 이벤트 발행 시점은 시스템의 데이터 일관성과 안정성에 큰 영향을 미칩니다. 일반적으로 이벤트는 Publisher의 Transaction이 성공적으로 commit된 후에 발행해야 합니다. 이는 Subscriber가 이벤트에 반응해 데이터를 조회하거나 처리할 때, Publisher의 작업이 완전히 완료된 상태를 보장하기 위함입니다.

만약 Transaction이 완료되기 전에 이벤트를 발행하면, Subscriber가 아직 확정되지 않은 데이터를 처리하려고 시도할 수 있어 데이터 일관성이 깨질 위험이 있습니다.

Publisher의 로직이 끝난 후 이벤트 발행이 중요한 이유

Publisher의 로직이 완료되고 Transaction이 commit된 후에 이벤트를 발행하는 것은 다음과 같은 이유로 중요합니다:

  1. 데이터 일관성 보장
    Transaction이 commit되기 전에 이벤트를 발행하면, Subscriber가 아직 DB에 반영되지 않은 데이터를 조회하려고 시도할 수 있습니다. 만약 Transaction이 롤백되면 Subscriber는 존재하지 않는 데이터를 처리하려고 하거나, 잘못된 상태를 기반으로 동작하게 됩니다.
  2. 오류 처리의 단순화
    Transaction이 실패해 롤백되면 이벤트가 발행되지 않도록 함으로써, Subscriber가 불필요한 작업을 수행하지 않게 됩니다. 이는 시스템의 안정성을 높이고 오류 상황을 관리하기 쉽게 만듭니다.
  3. 비즈니스 로직의 명확성
    Publisher의 작업이 완료된 후에만 이벤트가 발행되므로, Subscriber는 항상 확정된 상태에서 동작합니다. 이는 비즈니스 로직의 흐름을 명확히 하고, 예측 가능한 시스템을 만드는데 기여합니다.

Publisher의 로직이 끝나지 않은 상태에서 이벤트를 발행하면 어떤 문제가 생기나?

Publisher의 Transaction이 완료되기 전에 이벤트를 발행하면 여러 문제가 발생할 수 있습니다. 아래에서 구체적인 예시를 통해 이를 살펴보겠습니다.

문제1. Publisher가 Transaction이 진행중인 상태에서 Data A를 DB에 insert를 했다. Transaction이 끝나지 않은 상황에서 Publisher가 이벤트를 발행하고 Subscriber가 Data A에 접근하려고 하면 어떻게 될까?

Publisher가 Transaction 내에서 Data A를 DB에 삽입(insert)한 후, Transaction이 commit되기 전에 “Data A 생성 이벤트”를 발행했다고 가정해 봅시다. 이때 Subscriber가 이 이벤트를 받아 Data A를 조회하려고 하면 다음과 같은 문제가 발생할 수 있습니다:

  • 데이터 가시성 문제: Database의 격리 수준(Isolation Level)에 따라 다르지만, 일반적으로 commit되지 않은 데이터는 다른 Transaction에서 볼 수 없습니다. Subscriber가 Data A를 조회하려고 하면 데이터가 존재하지 않는 것처럼 보일 수 있습니다. 예를 들어, READ COMMITTED 격리 수준에서는 commit된 데이터만 조회 가능하므로, Subscriber는 Data A를 찾지 못합니다.

  • 롤백으로 인한 일관성 붕괴: Publisher의 Transaction이 실패해 롤백되면, Data A는 DB에서 삭제됩니다. 하지만 이미 이벤트가 발행되었기 때문에 Subscriber는 Data A를 기반으로 작업을 시도할 수 있습니다. 예를 들어, 이메일 발송 서비스가 고객에게 “Data A가 생성되었습니다”라는 알림을 보낼 수 있지만, 실제로는 Data A가 존재하지 않는 상황이 발생합니다.

  • 시스템 오류 증가: Subscriber가 조회한 데이터가 없거나 일관성이 깨진 상태에서 동작하면, 예외 처리가 필요해지고 시스템의 복잡성이 증가합니다. 이는 디버깅과 유지보수를 어렵게 만듭니다.

해결 방법

이러한 문제를 방지하려면, 이벤트 발행을 Transaction과 동기화해야 합니다. 아래는 실무에서 사용할 수 있는 두 가지 방법입니다:

  1. Transaction commit 후 이벤트 발행
    Spring Framework와 같은 프레임워크에서는 @TransactionalEventListener를 사용해 Transaction이 commit된 후 이벤트를 처리할 수 있습니다. 이를 통해 Publisher의 작업이 완료된 후에만 Subscriber가 동작하도록 보장합니다.
  2. 이벤트 저장소 활용
    Transaction 내에서 이벤트를 임시 테이블에 저장하고, Transaction이 commit된 후 별도의 프로세스가 이를 읽어 실제로 발행하는 방식입니다. 이는 이벤트 발행의 신뢰성을 높이고, Transaction과 이벤트를 분리하는 데 유용합니다.
This post is licensed under CC BY 4.0 by the author.