Skip to main content
대상 독자: 키오스크/CCU/카드단말 등 장치 클라이언트 개발자, 그리고 장치를 인증·수신하는 서비스(키오스크 API, 디바이스 게이트웨이) 개발자. 기존 audit 자동 캡처 체계(Prisma extension + Mutation 자동 래핑, ADR-0007 참고)는 core-svc의 GraphQL mutation과 DB 쓰기를 기준으로 동작한다. 물리 장치가 보내는 이벤트는 이 경로를 그대로 타지 않는 경우가 많아 — 화면 진입처럼 DB 쓰기가 없는 사용자 행위, 카드단말 불량처럼 core-svc mutation이 아닌 이벤트 — 별도 계약이 필요하다. 이 계약의 구체적 구현이 subscription-svcreportDeviceEvents다(§2).

1. 이벤트 분류 2종

categoryseverity(INFO/WARN/ERROR/CRITICAL)는 별도 분류 필드로 둔다 — actionType=EVENT 하나로 뭉뚱그리지 않는다. 화면 진입처럼 고볼륨인 BEHAVIOR 이벤트는 보존기간과 기본 조회 노출 정책(예: 기본 조회에서 제외, 별도 조회에서만 노출)을 DEVICE_HEALTH와 분리할 수 있어야 하기 때문이다.

2. 수집 경로 & 신뢰 경계 원칙

장치는 audit-svc에 직접 쓰지 않는다. 수신 서비스는 apps/subscription-svc에 신설하는 GraphQL 배치 mutation reportDeviceEvents 다 — 같은 서비스가 이미 publishAccommodationNotification으로 업장 알림을 처리하고 있고, 장치 인증 접근 제어(isKioskSelf/isCcuSelf, apps/subscription-svc/src/graphql/helpers/accessControl.ts)도 거기 갖춰져 있으므로 새 서비스를 만들지 않고 그 위에 얹는다. 용도 구분: publishAccommodationNotification은 프런트/키오스크 화면에 실시간으로 보여줄 알림(pubsub 구독 대상)이고, reportDeviceEvents는 화면에 보여주지 않는 이벤트 로그(audit 파이프라인 적재 전용)다. 알림이 필요 없는 화면 진입·장치 헬스 이벤트를 알림 mutation에 얹지 않는다. 호출 흐름:
  1. 키오스크/CCU가 게이트웨이를 경유해 장치 인증 컨텍스트(context.extra.user.kioskId 또는 .ccuId)를 갖고 reportDeviceEvents를 호출한다.
  2. resolver가 isKioskSelf/isCcuSelf로 요청의 deviceId가 인증된 장치 본인인지 검증한다 — requestAdultAuthentication(apps/subscription-svc/src/graphql/resolver/notification.ts:223)이 쓰는 것과 동일한 패턴: isKioskSelf(context.extra, { kioskId })가 거짓이면 즉시 거부.
  3. accommodationId요청 바디 값을 그대로 믿지 않는다. requestAdultAuthenticationkioskAccommodationId !== accommodationId일 때 KIOSK_ACCOMMODATION_MISMATCH로 거부하듯, subscription-svc가 인증된 장치의 등록 정보(context.extra.user.accommodationId)에서 조회한 값과 요청값을 대조해 불일치 시 거부한다.
  4. subscription-svc가 actorType/contextId 등을 enrich한 뒤 기존 audit 파이프라인(publishAuditLogBatch)으로 발행한다.
  • accommodationId, kioskId 등 장치 신원에 관한 값은 장치가 주장하는 값을 신뢰하지 않는다. resolver가 인증 컨텍스트에서 서버 측이 직접 조회해 채운다.
  • 장치가 보낸 페이로드의 accommodationId/kioskId 필드는 참고용으로만 취급하고, 서버가 조회한 값과 다르면 요청을 거부한다.

3. 클라이언트 전송 계약

장치가 이벤트를 보낼 때 요청에 실어야 하는 필드:

금지 필드 (payload에 절대 포함 금지)

카드번호/트랙데이터, 비밀번호, 인증 토큰 원문, 주민등록번호 등. 서버 측 캡처 계층도 재귀 마스킹(libs/audit/src/extension/mask.ts)을 거치지만, 클라이언트가 애초에 보내지 않는 것이 원칙이다 — 마스킹은 마지막 방어선이지 전송 허용 기준이 아니다.

배치 전송 API — reportDeviceEvents

오프라인 버퍼 플러시를 위해 단건이 아닌 배열 + 부분 성공 응답 형태를 기본으로 제공한다. subscription-svc의 GraphQL mutation이다.

4. 오프라인/재시도 시맨틱

  • 로컬 버퍼: 장치는 네트워크 단절 시 이벤트를 로컬에 버퍼링한다. 버퍼는 상한(건수)과 보존 시간을 둔다.
  • 재시도: 지수 백오프 + jitter로 재전송한다.
  • 전달 보장: at-least-once를 전제한다 — 서버가 eventId 기준 dedup을 책임지므로 클라이언트는 중복 전송을 두려워하지 않아도 된다.
  • 전송 우선순위: 버퍼가 가득 찼거나 네트워크가 간헐적일 때 우선순위 순서로 플러시한다.
    1. 결제/보안 관련 이벤트 (kiosk.payment.*, device.card_reader.*)
    2. 장치 헬스 이벤트
    3. 화면 이동 등 저우선순위 BEHAVIOR 이벤트
  • 버퍼 초과 시 드롭 정책: 오래된 BEHAVIOR 이벤트부터 드롭한다. DEVICE_HEALTH와 결제 관련 이벤트는 최대한 보존한다.

5. actorType 매핑 (확정)

AuditActorType(libs/audit/src/context/audit-context.ts)에 DEVICE를 추가한다. reportDeviceEvents는 이벤트별 category를 보고 enrich 단계에서 BEHAVIORKIOSK, DEVICE_HEALTHDEVICE로 매핑해 채운다.

6. 서버측 수신 서비스 체크리스트 (subscription-svc · reportDeviceEvents)

  1. 장치 신원 검증 — resolver가 인증 이전에 kioskId/ccuId정확히 1개만 제공됐는지 확인한다. 둘 다 없거나 둘 다 있으면 INVALID_DEVICE_IDENTITY로 즉시 거부.
  2. 장치 인증isKioskSelf/isCcuSelf(accessControl.ts)로 요청의 kioskId/ccuId가 인증 컨텍스트(context.extra.user)의 장치 본인인지 검증. 불일치 시 KIOSK_AUTH_REQUIRED 등으로 즉시 거부.
  3. accommodationId 대조 — 인증된 장치의 등록 accommodationId와 요청의 accommodationId를 대조, 불일치 시 KIOSK_ACCOMMODATION_MISMATCH로 거부(requestAdultAuthentication과 동일 패턴).
  4. 스키마 검증eventId/occurredAt/eventType/category 필수 필드와 eventType 카탈로그 존재 여부를 확인.
  5. eventId 멱등 — 이미 처리된 eventIdaccepted가 아니라 duplicated로 응답하고 재처리하지 않는다.
  6. EnrichaccommodationId(§2)/actorType(§5)/contextId를 서버가 직접 채운다. 장치가 보낸 값은 무시하거나 대조용으로만 쓴다.
  7. Audit 파이프라인 발행publishAuditLogBatch(@vpms-cluster/audit)를 호출한다. ADR-0007의 outbox 전환은 이 호출의 내부 구현만 바뀌므로 reportDeviceEvents 쪽 코드 변경은 없다.

7. 이벤트 예시

예시 1 — 화면 진입 배치 (BEHAVIOR)

reportDeviceEvents mutation 호출 변수:

예시 2 — 카드단말 불량 (DEVICE_HEALTH)

주의: 두 예시 모두 카드번호/트랙데이터를 포함하지 않는다. 카드단말 불량 페이로드는 장애 코드와 리더 식별자만 담고, 실제 카드 데이터는 절대 포함하지 않는다(§3 금지 필드 참고).