XOrder

POS Bridge

주문 플랫폼과 매장 POS 사이

주문 플랫폼과 매장 POS 사이를 중계하는 미들웨어다. 매장의 POS PC에 상주하는 에이전트가 서버와 상시 연결을 유지하면서, 플랫폼에서 생긴 주문을 POS로 내리고 POS의 메뉴·테이블 데이터를 서버로 끌어올린다. 실제 POS 장비가 없어서 통신 상대역을 하는 가상 POS를 같이 만들었다.

설계 완료 · 구현 초반

여섯 단계로 나눈 구현 계획 중 골격과 가상 POS까지 손댔다. 서버와 백오피스는 아직 뼈대뿐이고, 가상 POS 코드는 작업 PC에 해당 런타임이 깔려 있지 않아 빌드 검증도 못 한 상태다. 이 페이지에 적힌 건 만들어진 기능이 아니라 정해둔 설계다.

Constraints

구조를 결정한 다섯 가지

이 설계는 백지에서 나오지 않았다. 매장 연동이라는 문제를 오래 들여다보면 반복해서 마주치는 성질들이 있고, 항목 하나하나가 그중 하나에 대한 답이다.

01

홉이 하나 늘면 장애 지점도 하나 는다

플랫폼과 매장 사이에 중계 계층을 더 두면 구조는 유연해 보이지만, 끊겼을 때 어디서 끊겼는지 찾는 비용부터 올라간다.

그래서

중계 계층을 두지 않는다. 서버는 하나고, 연결은 에이전트가 밖으로 나가는 방향뿐이다.

02

갱신할 수 없는 프로그램은 현장에서 굳는다

매장에 설치된 데몬은 새 버전이 나와도 저절로 올라가지 않는다. 갱신 수단을 나중에 붙이려 하면 이미 옛 버전이 깔린 매장들이 그 수단을 못 받는 모순에 걸린다.

그래서

자가 업데이트 채널을 첫날 설계에 넣는다. 서버가 새 버전을 알리면 에이전트가 받아서 스스로 교체하고 재시작한다.

03

조용히 실패하는 동기화는 정산에서 터진다

마스터 데이터 수집은 실패해도 당장 아무 일도 일어나지 않는다. 그래서 재시도와 기록이 없으면 비어 있다는 사실을 한참 뒤에 알게 된다.

그래서

에이전트가 로컬 큐에 쌓고, 서버가 받았다고 답해야 큐에서 지운다. 동기화 작업마다 이력이 남는다.

04

중계 결과를 플랫폼이 모르면 아무도 책임질 수 없다

보냈는지, 도착했는지, 거절당했는지를 구분할 상태값이 없으면 장애가 났을 때 원인을 가릴 방법이 없다.

그래서

중계에 상태를 부여하고 전이를 전부 기록한다. 정해진 시간 안에 못 보내면 만료로 끊고, 플랫폼이 조회할 수 있게 한다.

05

스택을 두 벌 운영하면 부담도 두 벌이다

매장 데몬은 POS가 사는 환경에 맞춰야 하고, 서버는 플랫폼에 맞춰야 한다. 이 둘을 억지로 하나로 맞추면 어느 한쪽이 손해를 본다.

그래서

경계마다 다르게 고른다. 서버는 플랫폼과 같은 스택으로 통일하고, 매장 데몬 언어는 매장 PC 안에만 가둔다.

Topology

현장 제약이 만든 구조

매장은 남의 공간이고, POS는 남의 장비다. 이 두 제약이 구조를 거의 다 결정했다.

매장 네트워크를 건드리지 않는다

연결은 언제나 에이전트가 서버 쪽으로 나가는 방향뿐이다. 매장 공유기에 포트를 열거나 포스망 설정을 바꿀 일이 없다. 현장에서 이 요구는 협상 대상이 아니다.

POS와의 통신은 어댑터 뒤로

벤더마다 연동 방식이 다르다. 어떤 건 라이브러리 호출이고 어떤 건 로컬 소켓이다. 에이전트 본체는 어댑터 계약만 알고, 벤더 차이는 구현체 안에서 끝난다.

벤더 편차를 능력값으로 흡수

증분 조회를 지원하는지, 삭제를 표시해주는지, 변경 이벤트를 주는지를 어댑터가 스스로 신고한다. 증분을 못 하는 벤더면 동기화 엔진이 알아서 전체 조회로 내려간다.

프로토콜을 언어와 분리해서 문서로

에이전트의 본질은 서버와 주고받는 메시지 규약과 POS 어댑터 계약 두 가지다. 이걸 특정 언어 코드가 아니라 스펙 문서로 못 박아 두면, 나중에 안드로이드 POS용 에이전트가 필요해져도 같은 스펙을 다른 언어로 구현하는 문제가 된다.

State machine

주문 하나가 지나는 상태

'보냈다'와 '도착했다'는 다른 말이다. 그 사이를 상태로 쪼개두지 않으면 실패했을 때 아무도 무슨 일이 있었는지 모른다.

접수대기전송확정실패만료에이전트 온라인POS 거절 · 무응답제한 시간 초과
접수RECEIVED
플랫폼이 보낸 주문을 멱등키와 함께 받는다
대기QUEUED
해당 매장 에이전트가 온라인이면 큐에 올린다
전송SENT
에이전트로 내려보냈고 결과를 기다린다
확정CONFIRMED
POS가 받았다고 답했다
실패FAILED
POS가 거절했거나 응답이 오지 않았다
만료EXPIRED
에이전트가 계속 오프라인이라 시간 안에 못 보냈다

Safeguards

어긋나지 않게 하려고 넣은 것들

같은 주문이 두 번 들어가지 않게

플랫폼이 주는 멱등키를 서버가 유일 값으로 잡고, 에이전트도 중계 식별자로 한 번 더 막는다. 재연결 직후에는 에이전트가 '아직 결과를 못 준 주문이 뭔지'를 서버에 물어보고 자기 처리 이력과 대조한다.

주문은 추가만, 수정과 취소는 중계하지 않는다

주문 시스템 쪽 원칙을 그대로 가져왔다. 중계 구간에서 수정을 다루기 시작하면 두 시스템의 상태를 맞추는 문제가 되고, 그건 개인 프로젝트가 감당할 복잡도가 아니다.

벤더 원본은 정규화하지 않고 그대로 보관

POS가 준 데이터를 원형 그대로 한 겹에 쌓고, 거기서 공통 형태로 번역한 결과를 따로 둔다. 번역 규칙이 틀렸을 때 원본으로 돌아가서 다시 만들 수 있어야 해서다.

무엇이 왜 안 됐는지가 화면에 남는다

중계 하나를 열면 상태가 언제 어떻게 바뀌었는지 타임라인으로 보인다. 매장에서 문의가 들어왔을 때 로그를 뒤지지 않고 화면 하나로 답할 수 있어야 한다.

Virtual POS

장비가 없어서 상대역을 만들었다

실제 POS 장비가 없다. 그래서 어댑터부터 서버, 백오피스까지 전 구간을 실증할 통신 상대역을 직접 만들었다. 주문 접수 화면이나 결제 기능은 없다 — 오직 통신만 한다.

주입할 수 있는 장애

주문 응답 강제
성공 / 실패 / 무응답
응답 지연
임의의 밀리초
연결 강제 절단
1회성
POS 상태 전환
정상 / 장애

장애를 손으로 만들 수 있어야 복구를 보여줄 수 있다. 가상 POS가 존재하는 진짜 이유가 이쪽이다.

이걸로 확인하는 것

  • 정상 주문이 접수부터 확정까지 흐르는 것
  • POS에서 메뉴를 바꾸면 변경 이벤트가 나가고 동기화가 따라붙는 것
  • POS가 거절하거나 응답하지 않을 때 실패로 정리되는 것
  • 에이전트가 꺼져 있을 때 대기하다가, 살아나면 보내고 너무 늦으면 만료되는 것
  • 서버가 재시작해도 에이전트가 다시 붙고 올리던 데이터가 유실되지 않는 것
  • 새 버전을 올리면 에이전트가 스스로 갱신되는 것

이 목록은 시연 대본이자 각 단계의 완료 판정 기준이다. 통과하지 못하면 그 단계는 끝난 게 아니다.