이벤트는 버튼 클릭을 처리하는 문법이 아니다

vx_developer·2026년 9월 12일

개발하다가

목록 보기
21/30
post-thumbnail

프로그래밍을 처음 배울 때 이벤트는 보통 사용자가 버튼을 클릭하거나 키보드를 누르면 실행되는 동작이라고 배운다.

button.addEventListener(
  "click",
  () => {
    console.log(
      "버튼을 클릭했다."
    );
  }
);

사용자가 버튼을 클릭하면 등록된 함수가 실행된다.

이벤트와 이벤트 핸들러의 기본적인 관계를 이해하기에는 충분한 설명이다.

하지만 실제 서비스를 개발하기 시작하면 이벤트는 화면에서 발생하는 클릭에만 머물지 않는다.

쿠폰 서비스에서는 다음과 같은 일들이 모두 이벤트가 될 수 있다.

  • 사용자가 쿠폰 발행 버튼을 클릭했다.
  • 쿠폰 발행 요청이 제출되었다.
  • 쿠폰이 성공적으로 생성되었다.
  • 데이터베이스 저장이 완료되었다.
  • 쿠폰 수신자에게 알림을 보내야 한다.
  • 알림 전송이 실패했다.
  • 쿠폰의 마지막 사용 횟수가 소진되었다.
  • 결제 서비스에서 쿠폰 사용 완료 웹훅이 도착했다.

각 이벤트는 어떤 일이 발생했다는 사실을 나타내며, 시스템의 다른 반응을 시작할 수 있다.

따라서 실제 서비스에서는 더 많은 질문이 필요하다.

  • 이벤트는 사용자의 의도인가, 이미 발생한 사실인가?
  • 이벤트 이름만 보고 무엇이 일어났는지 알 수 있는가?
  • 이벤트에 어떤 데이터를 포함해야 하는가?
  • 누가 이벤트를 발생시키고 누가 처리하는가?
  • 핸들러를 즉시 실행해야 하는가?
  • 처리 실패는 원래 작업도 실패시켜야 하는가?
  • 같은 이벤트가 두 번 도착하면 어떻게 되는가?
  • 이벤트 순서가 바뀌면 어떤 문제가 생기는가?
  • 저장은 성공했지만 이벤트 발행은 실패하면 어떻게 되는가?

실제 서비스에서 이벤트는 버튼 클릭을 처리하는 문법이 아니다. 사용자와 시스템에서 발생한 사실을 표현하고, 그 사실에 필요한 반응을 책임별로 연결하는 방법이다.


클릭은 이벤트지만 서비스가 알고 싶은 최종 사실은 아니다

쿠폰 발행 버튼에 이벤트 핸들러를 등록할 수 있다.

<button
  onClick={handleClick}
>
  쿠폰 발행
</button>

이 코드는 사용자가 버튼을 클릭했다는 UI 이벤트를 처리한다.

하지만 클릭이 곧 쿠폰 발행 완료를 의미하지는 않는다.

async function handleClick() {
  await issueCoupon(formData);
}

요청 과정에서는 여러 결과가 발생할 수 있다.

  • 입력값 검증에 실패한다.
  • 사용자가 쿠폰을 발행할 권한이 없다.
  • 수신자를 찾을 수 없다.
  • 데이터베이스 저장에 실패한다.
  • 쿠폰 발행은 성공하지만 알림 전송은 실패한다.

따라서 다음 두 사건은 구분해야 한다.

사용자가 쿠폰 발행 버튼을 클릭했다.
쿠폰이 성공적으로 발행되었다.

첫 번째는 사용자의 의도를 보여주는 UI 이벤트다.

두 번째는 서비스 안에서 확인된 비즈니스 사실이다.

flowchart LR
    A[버튼 클릭]
    --> B[발행 요청]
    --> C[입력·권한·정책 확인]
    --> D[쿠폰 저장]
    --> E[쿠폰 발행 완료]

클릭 이벤트를 처리했다는 사실만으로 서비스 작업이 완료되었다고 판단해서는 안 된다.


이벤트와 명령은 서로 다른 의미를 가진다

사용자가 쿠폰 발행을 요청했다고 생각해 보자.

issueCoupon({
  title: "Coffee Date",
  recipientId: "user_chris",
});

이 코드는 아직 발생하지 않은 행동을 요청한다.

이를 명령이라고 볼 수 있다.

type IssueCouponCommand = {
  title: string;
  recipientId: string;
  issuerId: string;
};

명령은 다음과 같은 의미를 가진다.

이 작업을 수행해 달라.

반면 쿠폰이 성공적으로 발행된 뒤 다음 사실을 표현할 수 있다.

type CouponIssuedEvent = {
  couponId: string;
  recipientId: string;
  occurredAt: Date;
};

이벤트는 다음과 같은 의미를 가진다.

이 일이 이미 발생했다.

명령과 이벤트를 구분하면 이름도 자연스럽게 달라진다.

종류예시의미
명령IssueCoupon쿠폰을 발행해 달라
이벤트CouponIssued쿠폰이 발행되었다
명령RedeemCoupon쿠폰을 사용해 달라
이벤트CouponRedeemed쿠폰이 사용되었다
명령PauseCoupon쿠폰을 일시 정지해 달라
이벤트CouponPaused쿠폰이 일시 정지되었다

이벤트 이름을 완료된 사실처럼 표현하면 아직 실행되지 않은 요청과 이미 발생한 결과를 구분하기 쉽다.


이벤트 이름은 기술보다 서비스에서 일어난 일을 설명해야 한다

다음 이벤트 이름은 기술적 동작을 보여준다.

eventBus.publish(
  "database_row_inserted",
  data
);

데이터베이스 행이 추가되었다는 사실은 알 수 있지만 서비스에서 무슨 일이 일어났는지는 알기 어렵다.

  • 쿠폰이 발행되었는가?
  • 사용자가 가입했는가?
  • 주문이 생성되었는가?
  • 운영 로그가 저장되었는가?

서비스의 의미를 이름에 담을 수 있다.

eventBus.publish(
  "coupon.issued",
  {
    couponId: coupon.id,
    recipientId:
      coupon.recipientId,
    occurredAt: now,
  }
);

이제 구독자는 Prisma나 SQL을 몰라도 쿠폰이 발행되었다는 사실에 반응할 수 있다.

eventBus.subscribe(
  "coupon.issued",
  sendCouponNotification
);

이벤트 이름은 “어떤 코드가 실행되었는가?”보다 “서비스에서 어떤 사실이 생겼는가?”를 설명해야 한다.


이벤트는 상태 변화의 원인을 설명한다

현재 쿠폰 상태만 저장하면 지금 어떤 모습인지는 알 수 있다.

const coupon = {
  status: "completed",
  remainingUses: 0,
};

하지만 왜 완료되었는지는 알기 어렵다.

  • 마지막 횟수가 사용되었는가?
  • 운영자가 강제로 종료했는가?
  • 데이터 마이그레이션으로 변경되었는가?
  • 오류 복구 과정에서 수정되었는가?

상태를 바꾼 행동에서 이벤트를 만들 수 있다.

function redeemCoupon(
  coupon: Coupon,
  now: Date
) {
  const remainingUses =
    coupon.remainingUses - 1;

  const events: DomainEvent[] = [
    {
      type: "coupon.redeemed",
      couponId: coupon.id,
      occurredAt: now,
    },
  ];

  if (remainingUses === 0) {
    events.push({
      type: "coupon.completed",
      couponId: coupon.id,
      occurredAt: now,
    });
  }

  return {
    coupon: {
      ...coupon,
      remainingUses,
      status:
        remainingUses === 0
          ? "completed"
          : "active",
    },
    events,
  };
}

status: "completed"는 현재 결과를 보여준다.

coupon.completed는 그 결과를 만든 사실을 표현한다.

상태와 이벤트는 서로 경쟁하는 개념이 아니다.

  • 상태는 지금 어떤 모습인지 알려준다.
  • 이벤트는 어떤 변화가 발생했는지 알려준다.

하나의 이벤트는 여러 책임의 반응을 시작할 수 있다

쿠폰 발행 함수 안에서 모든 후속 작업을 직접 처리할 수 있다.

async function issueCoupon(
  input: IssueCouponInput
) {
  const coupon =
    await couponRepository.save(
      createCoupon(input)
    );

  await emailService.send(coupon);
  await analytics.track(coupon);
  await auditLog.write(coupon);
  await recommendation.refresh(
    coupon.recipientId
  );

  return coupon;
}

쿠폰 발행 사용 사례가 이메일, 분석, 감사 로그, 추천 시스템을 모두 알고 있다.

후속 기능이 추가될수록 이 함수의 책임과 실패 가능성이 커진다.

쿠폰 발행이라는 사실을 이벤트로 표현할 수 있다.

await eventBus.publish({
  type: "coupon.issued",
  couponId: coupon.id,
  recipientId:
    coupon.recipientId,
  occurredAt: now,
});

각 책임은 필요한 반응을 등록한다.

eventBus.subscribe(
  "coupon.issued",
  sendCouponNotification
);

eventBus.subscribe(
  "coupon.issued",
  recordCouponAnalytics
);

eventBus.subscribe(
  "coupon.issued",
  writeCouponAuditLog
);

이제 쿠폰 발행 로직은 후속 작업의 모든 구현을 알 필요가 줄어든다.

하지만 결합이 완전히 사라진 것은 아니다.

발행자는 이벤트의 계약에 의존하고, 구독자는 그 이벤트가 발생한다는 사실에 의존한다.

이벤트는 의존성을 없애는 도구가 아니라 직접적인 호출 관계를 사실 중심의 계약으로 바꾸는 방법이다.


모든 후속 작업을 이벤트로 분리해야 하는 것은 아니다

쿠폰을 저장하기 전에 반드시 수신자가 존재하는지 확인해야 한다고 생각해 보자.

const recipient =
  await userRepository.findById(
    input.recipientId
  );

if (!recipient) {
  throw new RecipientNotFoundError();
}

이 확인을 비동기 이벤트 핸들러로 넘기면 쿠폰 발행 함수는 결과를 기다리지 않을 수 있다.

eventBus.publish({
  type: "recipient.validation_requested",
  recipientId:
    input.recipientId,
});

수신자가 없더라도 쿠폰이 먼저 저장될 수 있다.

쿠폰 발행의 성공 조건에 필요한 작업은 이벤트 뒤로 숨기면 안 된다.

다음과 같이 구분할 수 있다.

작업처리 방식
입력과 권한 검증발행 전에 직접 처리
쿠폰 생성 규칙 적용발행 작업 안에서 처리
데이터베이스 저장성공 조건에 포함
알림 전송정책에 따라 후속 이벤트 처리 가능
분석 데이터 기록후속 이벤트 처리 가능
추천 정보 갱신후속 이벤트 처리 가능

이벤트를 사용할지는 “코드를 분리하고 싶은가?”만으로 결정하지 않는다.

이 반응이 원래 작업의 성공 조건에 포함되는가?

이 질문에 따라 직접 호출과 이벤트 처리를 선택해야 한다.


이벤트에 모든 데이터를 넣으면 과거의 내부 구조가 계약이 된다

쿠폰 발행 이벤트에 쿠폰 객체 전체를 포함할 수 있다.

eventBus.publish({
  type: "coupon.issued",
  coupon,
});

간단해 보이지만 쿠폰 객체의 모든 필드가 이벤트 소비자에게 공개된다.

나중에 쿠폰 모델이 바뀌면 여러 소비자가 영향을 받을 수 있다.

민감하거나 필요하지 않은 값도 전달될 수 있다.

type CouponIssuedEvent = {
  eventId: string;
  couponId: string;
  recipientId: string;
  occurredAt: string;
  version: 1;
};

이벤트를 처리하는 데 필요한 의미만 포함하면 계약의 범위를 줄일 수 있다.

반대로 ID만 전달하면 소비자가 다시 데이터를 조회해야 한다.

const coupon =
  await couponRepository.findById(
    event.couponId
  );

이때 조회한 쿠폰은 이벤트가 발생한 시점과 다른 상태일 수 있다.

따라서 이벤트 데이터는 항상 최소화하면 된다는 뜻도 아니다.

다음 질문으로 결정해야 한다.

  • 소비자가 과거 시점의 값을 알아야 하는가?
  • 현재 상태를 다시 조회해도 되는가?
  • 어떤 필드가 이벤트의 의미를 완성하는가?
  • 개인 정보나 비밀값이 포함되는가?
  • 계약 변경을 어떻게 관리할 것인가?

화면 이벤트와 도메인 이벤트는 같은 이름을 사용해도 책임이 다르다

React 컴포넌트에서 쿠폰 선택 이벤트를 처리할 수 있다.

<CouponCard
  onSelect={() =>
    setSelectedCouponId(
      coupon.id
    )
  }
/>

이 이벤트는 컴포넌트 사이의 UI 상호작용을 연결한다.

데이터베이스에 영구 기록할 필요는 없다.

반면 쿠폰 사용 완료 이벤트는 서비스의 사실을 표현한다.

type CouponRedeemedEvent = {
  couponId: string;
  userId: string;
  occurredAt: string;
};

이 이벤트는 알림, 감사 기록, 분석 또는 다른 서비스와 연결될 수 있다.

이벤트 종류예시주요 범위
DOM 이벤트click, submit브라우저와 요소
컴포넌트 이벤트onSelect, onCloseUI 컴포넌트 사이
도메인 이벤트coupon.redeemed한 서비스의 비즈니스 규칙
통합 이벤트coupon.redemption.completed서비스 사이의 계약

모두 이벤트라는 이름을 사용하지만 생명주기와 신뢰 수준, 실패 처리 방식은 다르다.

하나의 이벤트 시스템으로 모든 종류를 동일하게 다룰 필요는 없다.


동기 이벤트와 비동기 이벤트는 실패의 의미가 다르다

메모리 안에서 이벤트 핸들러를 즉시 실행할 수 있다.

await eventBus.publish(
  couponIssued
);

publish가 모든 핸들러의 완료를 기다린다면 알림 전송 실패가 쿠폰 발행 요청까지 실패시킬 수 있다.

쿠폰 저장 성공
→ 이메일 전송 실패
→ API 응답 실패

사용자는 다시 버튼을 누를 수 있고, 쿠폰이 중복 발행될 수도 있다.

메시지 큐에 이벤트를 저장하고 후속 작업을 나중에 처리할 수도 있다.

await eventQueue.enqueue(
  couponIssued
);

이 경우 쿠폰 발행 응답은 먼저 성공할 수 있다.

알림은 잠시 뒤 처리되거나 재시도된다.

선택 기준은 단순히 성능이 아니다.

  • 후속 작업이 끝나야 원래 작업이 성공한 것인가?
  • 잠시 지연되어도 되는가?
  • 실패하면 재시도해야 하는가?
  • 사용자가 즉시 결과를 알아야 하는가?
  • 후속 작업의 결과를 어디에서 확인할 수 있는가?

비동기 이벤트는 실패를 없애지 않는다.

실패가 발생하는 위치와 시점을 바꾸며, 재시도와 모니터링 책임을 추가한다.


이벤트는 중복되고 순서가 달라질 수 있다고 생각해야 한다

메시지 시스템은 처리 실패를 복구하기 위해 같은 이벤트를 다시 전달할 수 있다.

async function handleCouponIssued(
  event: CouponIssuedEvent
) {
  await sendNotification(
    event.recipientId
  );
}

같은 이벤트가 두 번 처리되면 수신자에게 같은 알림이 두 번 전송될 수 있다.

이벤트 ID를 사용해 이미 처리했는지 확인할 수 있다.

if (
  await processedEventRepository
    .exists(event.eventId)
) {
  return;
}
await sendNotification(
  event.recipientId
);

await processedEventRepository
  .save(event.eventId);

가능하면 처리 작업과 이벤트 기록을 하나의 트랜잭션으로 보호하거나, 외부 API가 제공하는 멱등성 키를 사용할 수 있다.

순서도 보장되지 않을 수 있다.

coupon.paused
coupon.resumed

네트워크 상황에 따라 resumed가 먼저 도착하면 오래된 paused 이벤트가 최종 상태를 덮어쓸 수 있다.

이벤트 버전이나 대상 데이터의 버전을 비교할 수 있다.

if (
  event.aggregateVersion <=
  currentVersion
) {
  return;
}

이벤트 기반 처리를 설계할 때는 정확히 한 번만, 항상 순서대로 도착한다고 가정하지 않는 편이 안전하다.


데이터 저장과 이벤트 발행 사이에는 빈틈이 생길 수 있다

쿠폰을 데이터베이스에 저장한 뒤 이벤트를 발행한다고 생각해 보자.

const coupon =
  await couponRepository.save(
    newCoupon
  );

await eventBus.publish({
  type: "coupon.issued",
  couponId: coupon.id,
});

쿠폰 저장은 성공했지만 프로세스가 이벤트 발행 전에 중단될 수 있다.

그러면 쿠폰은 존재하지만 알림과 분석 작업은 시작되지 않는다.

반대로 이벤트를 먼저 발행하면 구독자가 아직 저장되지 않은 쿠폰을 조회할 수 있다.

데이터 변경과 발행할 이벤트를 같은 데이터베이스 트랜잭션에 기록하는 방법을 사용할 수 있다.

await database.transaction(
  async (transaction) => {
    await transaction.coupon.create({
      data: coupon,
    });

    await transaction.outbox.create({
      data: {
        type: "coupon.issued",
        payload: {
          couponId: coupon.id,
        },
      },
    });
  }
);

별도의 작업자가 아웃박스에 기록된 이벤트를 메시지 시스템으로 전달한다.

flowchart LR
    A[쿠폰 저장 트랜잭션]
    --> B[쿠폰 행 저장]
    A --> C[아웃박스 이벤트 저장]
    C --> D[이벤트 발행 작업자]
    D --> E[메시지 시스템]
    E --> F[알림·분석 처리]

모든 서비스에 아웃박스 패턴이 필요한 것은 아니다.

하지만 데이터 저장과 이벤트 발행이 모두 빠지면 안 되는 중요한 작업이라면 두 작업 사이의 실패 가능성을 고려해야 한다.


이벤트 핸들러가 늘어나면 보이지 않는 동작도 늘어난다

다음 코드는 쿠폰 하나를 발행한다.

await issueCoupon(input);

하지만 coupon.issued에 많은 핸들러가 연결되어 있다면 실제로는 여러 작업이 실행될 수 있다.

  • 이메일을 보낸다.
  • 푸시 알림을 보낸다.
  • 감사 기록을 남긴다.
  • 분석 데이터를 전송한다.
  • 추천 캐시를 갱신한다.
  • 외부 파트너에게 웹훅을 보낸다.

호출부만 읽어서는 전체 영향을 알기 어려워진다.

이벤트를 사용하면 발행자와 소비자의 직접 결합은 줄어들 수 있지만 실행 흐름의 가시성도 낮아질 수 있다.

따라서 다음 정보가 필요하다.

  • 어떤 코드가 이벤트를 발행하는가?
  • 현재 등록된 소비자는 무엇인가?
  • 각 소비자의 처리 상태는 무엇인가?
  • 몇 번 재시도했는가?
  • 최종 실패한 이벤트는 어디에 남는가?
  • 하나의 이벤트를 끝까지 추적할 ID가 있는가?
logger.info(
  "coupon_event_processed",
  {
    eventId: event.eventId,
    eventType: event.type,
    handler:
      "sendCouponNotification",
  }
);

좋은 이벤트 설계는 코드를 느슨하게 연결하는 것뿐 아니라 운영 중 흐름을 다시 추적할 수 있게 만든다.


이벤트를 설계하기 전에 물어봐야 할 질문

의미

  1. 이 이벤트는 사용자의 의도인가, 이미 발생한 사실인가?
  2. 이벤트 이름만 읽어도 서비스에서 일어난 일을 알 수 있는가?
  3. 기술 동작보다 비즈니스 의미를 표현하는가?
  4. 상태 변화의 원인을 설명하는가?
  5. 이벤트가 발생했다고 말할 수 있는 정확한 시점은 언제인가?

발행자와 소비자

  1. 누가 이 사실을 확정하고 이벤트를 발행하는가?
  2. 어떤 책임들이 이 이벤트에 반응해야 하는가?
  3. 발행자가 소비자의 존재를 알아야 하는가?
  4. 소비자가 추가되면 원래 기능의 의미가 바뀌는가?
  5. 소비자 하나의 실패가 다른 소비자에게 영향을 줘야 하는가?

데이터 계약

  1. 소비자가 결정을 내리는 데 필요한 데이터는 무엇인가?
  2. 전체 내부 객체를 불필요하게 공개하고 있지는 않은가?
  3. ID만 전달한 뒤 현재 데이터를 다시 조회해도 되는가?
  4. 이벤트에 민감한 정보가 포함되어 있지는 않은가?
  5. 계약 버전 변경을 어떻게 관리할 것인가?

실행과 실패

  1. 동기 처리가 필요한가, 비동기 처리가 가능한가?
  2. 후속 작업이 원래 요청의 성공 조건인가?
  3. 실패한 이벤트를 재시도해야 하는가?
  4. 같은 이벤트를 여러 번 처리해도 안전한가?
  5. 이벤트 순서가 바뀌거나 늦게 도착해도 안전한가?

저장과 운영

  1. 데이터 저장과 이벤트 발행 사이의 실패를 어떻게 처리하는가?
  2. 처리되지 못한 이벤트는 어디에 보관되는가?
  3. 이벤트와 핸들러를 끝까지 추적할 수 있는가?
  4. 재시도 횟수와 최종 실패를 모니터링하는가?
  5. 이벤트가 늘어나도 전체 실행 흐름을 이해할 수 있는가?

흔히 하는 실수

버튼 클릭을 비즈니스 성공으로 생각한다

button.onclick = () => {
  analytics.track(
    "coupon_issued"
  );
};

클릭 후 검증이나 저장이 실패할 수 있다.

const coupon =
  await issueCoupon(input);

analytics.track(
  "coupon_issued",
  {
    couponId: coupon.id,
  }
);

성공이 확정된 시점의 사실을 기록해야 한다.


명령과 이벤트를 같은 이름으로 표현한다

eventBus.publish(
  "issue_coupon",
  data
);

요청인지 완료된 사실인지 불분명하다.

execute(
  "IssueCoupon",
  command
);

publish(
  "CouponIssued",
  event
);

수행 요청과 발생한 결과를 구분한다.


이벤트에 내부 객체 전체를 넣는다

publish({
  type: "coupon.issued",
  coupon,
});

내부 구조와 민감한 정보가 소비자 계약으로 퍼질 수 있다.

publish({
  type: "coupon.issued",
  eventId,
  couponId: coupon.id,
  recipientId:
    coupon.recipientId,
  occurredAt,
});

이벤트의 의미에 필요한 데이터만 계약으로 제공한다.


성공 조건까지 비동기 이벤트에 맡긴다

publish({
  type:
    "coupon.validation_requested",
});

필수 검증이 끝나기 전에 쿠폰이 저장될 수 있다.

원래 작업이 성공하기 위해 반드시 필요한 규칙은 작업 내부에서 확인한다.


이벤트가 한 번만 도착한다고 가정한다

await giveReward(
  event.userId
);

재전달되면 보상이 중복될 수 있다.

이벤트 ID, 고유 제약 또는 멱등성 키를 이용해 반복 처리를 안전하게 만든다.


이벤트를 발행하면 책임이 끝났다고 생각한다

await eventQueue.enqueue(
  event
);

큐에 넣는 데 성공해도 소비자가 처리하지 못할 수 있다.

재시도, 최종 실패 저장소, 모니터링과 추적 방식을 함께 설계해야 한다.


모든 함수 호출을 이벤트로 바꾼다

publish("coupon_title_trimmed");
publish("coupon_id_created");
publish("coupon_object_built");

하나의 작업을 이해하기 위해 지나치게 많은 간접 흐름을 따라가야 한다.

다른 책임이 실제로 반응해야 하는 중요한 사실인지 확인한 뒤 이벤트로 표현한다.


핵심 정리

이벤트는 어떤 일이 발생했을 때 코드를 실행하도록 연결하는 방법이다.

button.addEventListener(
  "click",
  handleClick
);

입문 단계에서는 버튼 클릭과 핸들러의 관계를 이해하는 것으로 충분하다.

하지만 실제 서비스의 이벤트는 화면 상호작용보다 넓은 의미를 가진다.

type CouponIssuedEvent = {
  eventId: string;
  couponId: string;
  recipientId: string;
  occurredAt: string;
};

CouponIssued는 버튼을 눌렀다는 뜻이 아니다.

검증과 비즈니스 규칙을 통과하고 쿠폰이 실제로 발행되었다는 사실을 의미한다.

좋은 이벤트 설계는 다음 내용을 분명하게 만든다.

  • 사용자의 요청과 이미 발생한 사실의 차이
  • 이벤트가 확정되는 시점
  • 이벤트를 발행할 책임
  • 이벤트에 반응할 책임
  • 소비자에게 필요한 데이터 계약
  • 동기 또는 비동기 처리 여부
  • 실패와 재시도 방식
  • 중복과 순서 변경에 대한 처리
  • 데이터 저장과 이벤트 발행의 일관성
  • 운영 중 전체 흐름을 추적하는 방법

이벤트의 흐름은 다음과 같이 볼 수 있다.

flowchart LR
    A[사용자 또는 시스템 행동]
    --> B[비즈니스 규칙 확인]
    --> C[상태 변경 확정]
    --> D[이벤트 발행]
    --> E[독립된 책임의 반응]
    --> F[재시도와 모니터링]

이벤트를 설계한다는 것은 결국 다음 질문에 답하는 일이다.

서비스에서 어떤 사실이 발생했으며, 그 사실을 알아야 하는 책임들이 어떻게 안전하고 추적 가능한 방식으로 반응하게 할 것인가?

이벤트는 버튼 클릭을 처리하는 문법이 아니다.

사용자 행동과 시스템의 상태 변화를 의미 있는 사실로 표현하고, 그 사실에 필요한 반응을 연결하는 방법이다.

profile
Vision eXperience Developer

0개의 댓글