# 제품 간 잔액 동기화: 다른 제품의 실시간 잔액을 WebSocket으로 표시

> 제품 A가 사용자 잔액을 제품 B로 옮기고 A의 앱이 B의 잔액을 보여 줄 때, A의 백엔드를 거치는 폴링은 A의 처리 능력을 B의 지연에 묶어 둡니다. B가 변경마다 WebSocket으로 푸시해 해결하는 방법을 흐름도로 설명합니다.

Source: https://hibira.io/ko/topics/balance-sync-websocket
Updated: 2026-11-09

두 제품 사이에서 흔한 구성입니다. 사용자가 잔액 일부를 제품 A에서 제품 B로 옮긴 뒤 B에서 씁니다. 사용자가 B에서 쓰거나 얻는 동안에도 A의 앱은 B에 얼마가 남았는지 실시간으로 보여 줘야 합니다. 첫 버전은 보통 HTTP로 물어봅니다. A의 앱이 A의 백엔드를 폴링하고, A의 백엔드가 B에 묻습니다. 폴링할 때마다 두 제품을 가로지르는 요청 사슬이 생기고, 트래픽이 늘면 몇 명의 사용자를 감당할 수 있는지를 정하는 것은 어느 제품도 아닌 이 사슬이 됩니다.

## 요청 사슬은 이렇게 쌓입니다

```text
flow: 변경 전: 사용자마다 몇 초에 한 번, A의 앱이 A의 백엔드에 묻고 A의 백엔드가 B에 물음
== 잔액 옮기기
A의 앱 --POST /transfer--> A의 백엔드|B를 기다림 --POST /deposit--> B의 백엔드
== A의 앱에 B의 잔액 표시
A의 앱|5초마다 --GET /b-balance--> A의 백엔드|B를 기다림 --GET /balance--> B의 백엔드|요청마다 worker 하나
A의 앱|5초마다 --GET /b-balance--> A의 백엔드|B를 기다림 --GET /balance--> B의 백엔드|요청마다 worker 하나
```

HTTP 요청은 기다리는 동안 무언가를 붙잡고 있습니다. worker 스레드, 클라이언트 커넥션 풀의 연결, 흔히 데이터베이스 연결까지요. A의 요청은 B가 응답해야 응답할 수 있으므로, 자기 작업 시간에 B가 걸리는 시간 전체를 더한 만큼 그것들을 붙잡습니다. [리틀의 법칙](https://en.wikipedia.org/wiki/Little%27s_law)으로 숫자를 낼 수 있습니다. 처리 중인 요청 수 = 초당 요청 수 × 요청 하나에 걸리는 초입니다. 게다가 B는 페이지를 열어 둔 A의 사용자마다 몇 초에 한 번씩 요청을 받습니다. 잔액이 바뀌었든 아니든 말이죠.

## worker가 얼마나 드는가

벤치마크가 아니라 계산 예시입니다(2026-11-09 작성). A의 사용자 5,000명의 화면에 B의 잔액이 떠 있고 A의 앱은 5초마다 폴링하므로, 초당 1,000개의 요청이 A를 거쳐 B로 갑니다.

| | A의 백엔드를 거치는 폴링 | B가 변경마다 WebSocket으로 푸시 |
| --- | --- | --- |
| 초당 A와 B에 도착하는 잔액 요청 | 각각 1,000개, 대부분 변화 없음 | 없음, 앱이 연결할 때 한 번 읽음 |
| 요청 하나가 A의 worker를 붙잡는 시간 | 자체 작업 10 ms + B 호출 60 ms = 70 ms | A를 전혀 거치지 않음 |
| B의 잔액을 중계하느라 바쁜 A의 worker 수 | 1,000 × 0.07초 = 70 | 0 |
| B가 600 ms로 느려졌을 때 | 1,000 × 0.61초 = 610, 200개짜리 풀의 세 배 | 그대로 0: 다음 업데이트가 조금 늦게 올 뿐 |

아픈 곳은 마지막 줄입니다. 풀이 worker 200개라면 B가 잠깐 느려지는 것만으로 A의 풀이 바닥납니다. 로그인과 결제를 포함한 A 자신의 요청이 큐에서 기다리다 타임아웃되고, 클라이언트가 재시도하면서 요청이 더 늘어납니다. A의 동시 처리량은 하필 트래픽이 가장 많을 때 떨어지고, 그 원인은 A가 운영하지도 않는 제품입니다. 그동안 B도 대부분 "변화 없음"인 폴링에 자기 처리 능력을 씁니다.

## 변경 후: B가 변경마다 한 번만 발행

```text
flow: 변경 후: B가 사용자 잔액의 모든 변경을 발행하고, A의 앱이 직접 받음
A의 앱 --POST /transfer--> A의 백엔드 --POST /deposit--> B의 백엔드|B의 원장에 기록
B의 백엔드|변경할 때마다 --balance.updated 발행--> *Hibira|private-b-balance-42 --WebSocket--> A의 앱|그리고 B 자신의 앱
```

```text
sequence: 사용자가 1,000을 B로 옮기고 B에서 120을 씀, A의 앱은 둘 다 바로 봄
A의 앱 | A의 백엔드 | B의 백엔드 | *Hibira
1 > 2: 1,000을 B로
2 > 3: POST /deposit | 멱등 키
3 >> 2: 200 | B 잔액 1,000, 버전 1
3 > 4: balance.updated | private-b-balance-42
4 > 1: balance.updated | 1,000, 버전 1
note: 나중에 사용자가 B에서 120을 씀
3 > 4: balance.updated | 880, 버전 2
4 > 1: balance.updated | 880, 버전 2
```

B는 변경을 기록할 때마다, 입금을 포함해 한 번 발행합니다. A의 앱은 WebSocket 연결 하나를 유지하고 네트워크 왕복 한 번 만에 새 잔액을 받습니다. B의 잔액은 더 이상 A의 백엔드를 거치지 않으므로 느리거나 바쁜 B가 A의 worker를 붙잡을 수 없고, B도 수천 건의 폴링에 응답할 필요가 없어집니다.

## 설정 방법

- **B의 잔액은 B가 책임집니다.** B에 얼마가 남았는지의 기준은 B의 원장입니다. 커밋할 때마다 B는 아무 Pusher 서버 라이브러리의 HTTP API로 새 잔액과 버전 번호를 담은 `balance.updated`를 발행합니다.
- **사용자마다 비공개 채널 하나.** 예를 들어 `private-b-balance-42`입니다. A와 B가 같은 앱을 쓴다면 A의 인증 엔드포인트가 로그인한 사용자가 42인지 확인한 뒤 구독에 서명합니다. B가 자기 앱을 운영한다면 A의 앱은 B의 앱 키로 연결하고 B의 인증 엔드포인트가 서명합니다. 중계 서버가 암호문만 보게 하려면 `private-encrypted-b-balance-42`를 쓰세요. 다른 비공개 채널은 전송 중에는 TLS로 보호되고 저장 시 암호화됩니다.
- **버전 번호를 쓰세요.** 화면에 있는 것보다 낮은 버전의 이벤트는 무시합니다. A의 앱이 연결하거나 재연결하면 HTTP로 B의 잔액을 한 번 읽고 그다음 이벤트를 적용합니다.
- **A의 백엔드도 받을 수 있습니다.** 예를 들어 사용자가 돈을 되돌리기 전처럼 A가 서버에서 B의 잔액이 필요하다면, 매번 B를 호출하는 대신 Pusher 클라이언트 라이브러리로 같은 이벤트를 받아 사본을 최신으로 유지할 수 있습니다.

## 그대로인 것

A에서 B로 옮기는 것은 여전히 멱등 키가 붙은 HTTP 요청이므로 재시도해도 돈이 두 번 옮겨지지 않고, B도 기록할 때마다 직접 잔액을 확인합니다. WebSocket이 나르는 것은 B의 잔액이 바뀌었다는 소식이지 변경 자체가 아닙니다. 반대편 제품이 외부 API 제공업체라면 이체 자체도 큐를 넘치게 할 수 있습니다. [API 제공업체와의 지갑 이체](https://hibira.io/ko/topics/wallet-transfers-api-providers)를 보세요.

## 자주 묻는 질문

### 폴링 간격을 늘리면 되지 않나요?

간격을 늘리면 요청은 줄지만 잔액은 오래된 값이 됩니다. B에서 얻은 금액이 A의 앱에 간격 하나만큼 늦게 나타나고, 매번의 폴링은 여전히 B를 기다립니다. 푸시는 네트워크 왕복 한 번이면 되고, 바뀐 것이 없으면 아무것도 보내지 않습니다.

### 연결이 끊긴 동안 A의 앱이 이벤트를 놓치면요?

pusher-js와 다른 클라이언트 라이브러리는 스스로 재연결합니다. 재연결 후 HTTP로 B의 잔액을 한 번 읽고 그보다 높은 버전의 이벤트만 적용하세요. 버전 번호가 있으면 중복되거나 늦게 온 이벤트도 문제가 되지 않습니다.

### A가 B의 코드를 바꿔야 하나요?

B에 필요한 추가는 하나입니다. 잔액을 기록한 뒤 `balance.updated`를 발행하고, 자기 앱을 운영한다면 구독에 서명하는 것입니다. 이체 API는 그대로입니다.

### Hibira에서는 이 이벤트를 어떻게 세나요?

발행 한 번이 메시지 하나입니다. 유료 플랜에서는 구독자에게 가는 전달을 세지 않으므로, 같은 잔액을 A와 B의 앱에 모두 보여 줘도 한쪽에만 보여 주는 것과 비용이 같습니다. [요금](https://hibira.io/ko/pricing)을 보세요.
