---
term: 'Pub/Sub(발행-구독) 패턴'
seoTitle: 'Pub/Sub 패턴이란? 토픽, 브로커, 전달 보장'
headline: 'Pub/Sub(발행-구독) 패턴이란?'
slug: pub-sub-pattern
category: api-realtime
shortDefinition: 'Pub/sub은 발행자가 브로커의 토픽으로 메시지를 보내고 그 토픽의 모든 구독자가 사본을 받는 메시징 패턴입니다.'
relatedTerms:
  - event-driven-architecture
  - websockets-real-time-sync
  - real-time-live-queries
  - sse-vs-websockets-vs-polling
contrastsWith:
  - webhooks
aboutTerms:
  - 'Publisher'
  - 'Subscriber'
  - 'Topic'
  - 'Message Broker'
faq:
  - question: 'pub/sub을 쉽게 설명하면 무엇입니까?'
    answer: '송신자(발행자)가 브로커의 이름 있는 토픽에 메시지를 게시하고, 그 토픽을 구독한 모든 구성 요소가 각자의 사본을 받는 메시징 패턴입니다. 발행자와 구독자는 서로의 존재를 전혀 모릅니다. 라디오 방송을 떠올리면 됩니다. 방송국은 한 주파수로 송출하고, 그 주파수에 맞춘 사람은 누구나 듣습니다.'
  - question: 'pub/sub과 메시지 큐의 차이는 무엇입니까?'
    answer: '큐는 작업을 분배하고, pub/sub은 작업을 복제합니다. 큐에서는 각 메시지를 정확히 하나의 워커가 소비하므로 작업을 워커 풀에 나누는 도구가 됩니다. pub/sub에서는 토픽의 모든 구독자가 사본을 받으므로 각자 관심 있는 여러 당사자에게 이벤트를 방송하는 도구가 됩니다.'
  - question: 'pub/sub과 옵저버 패턴의 차이는 무엇입니까?'
    answer: '옵저버는 프로세스 내부에서 동작합니다. 주체(subject)가 옵저버에 대한 직접 참조를 갖고 보통 동기적으로 호출합니다. pub/sub은 당사자 사이에 브로커를 끼워 넣어 서로를 완전히 분리하며, 대개 비동기로 동작하고, 서로 다른 프로세스, 언어, 배포 단위에 존재할 수 있게 합니다. 하나는 앱 내부의 디자인 패턴이고, 다른 하나는 시스템 사이의 아키텍처입니다.'
  - question: 'pub/sub에서 토픽이란 무엇입니까?'
    answer: '메시지를 분류하는 이름 있는 논리 채널입니다. 발행자는 토픽을 지정해 보내고, 브로커는 각 메시지를 그 토픽의 모든 구독자에게 전달합니다. 필터링은 토픽 기반(이름으로 구독하며 흔히 orders.* 형태의 계층 구조)이거나, 브로커가 메시지 속성을 기준으로 매칭하는 콘텐츠 기반일 수 있습니다.'
  - question: 'pub/sub은 어떤 전달 보장을 제공합니까?'
    answer: '세 단계입니다. 최대 한 번(at-most-once)은 발행 후 망각 방식으로 빠르지만 메시지를 잃을 수 있습니다. 최소 한 번(at-least-once)은 확인(ack)될 때까지 재시도하는 흔한 기본값이며, 가끔 중복을 만듭니다. 정확히 한 번(exactly-once)은 중복 제거와 조정을 결합한 방식으로 비용이 크고 제약이 많으며, 일반적으로 종단 간 보장은 불가능합니다. 실용적인 입장은 최소 한 번 전달과 멱등한 핸들러의 조합입니다.'
  - question: 'pub/sub은 메시지 순서를 보장합니까?'
    answer: '기본적으로는 보장하지 않습니다. 메시지는 병렬 구독자에게 팬아웃되며, 전역 순서는 병렬성과 충돌합니다. 브로커는 처리량을 희생해 파티션이나 순서 키 단위로 순서를 보장할 수 있습니다. 견고한 설계는 명시적으로 설정한 곳에서만 순서를 가정하고, 순서가 뒤바뀌어 도착해도 견디는 핸들러를 작성합니다.'
  - question: 'Kafka는 pub/sub입니까?'
    answer: '그렇습니다. 다만 한 가지 차이가 있습니다. Apache Kafka는 파티션으로 나뉘고 내구성이 있으며 재생 가능한 커밋 로그 위에 발행-구독 의미론을 구현합니다. 소비자 그룹 안에서는 큐처럼 동작하고(각 메시지가 한 멤버에게), 그룹 사이에서는 pub/sub처럼 동작합니다(각 그룹이 사본을 받음). 그래서 보통 이벤트 스트리밍이라고 부릅니다.'
  - question: 'pub/sub을 사용하지 말아야 할 때는 언제입니까?'
    answer: '호출자가 즉시 답을 받아야 할 때(요청-응답), 생산자와 소비자에 걸쳐 작업이 원자적이어야 할 때(브로커는 트랜잭션에 참여할 수 없으므로 사가 패턴을 참고), 엄격한 전역 순서가 필요할 때, 또는 시스템이 작아서 브로커가 줄여 주는 운영 부담보다 더하는 부담이 더 클 때입니다.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'Publish-Subscribe Channel — Enterprise Integration Patterns'
    url: 'https://www.enterpriseintegrationpatterns.com/patterns/messaging/PublishSubscribeChannel.html'
  - name: 'MQTT 5.0 specification — OASIS'
    url: 'https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html'
  - name: 'Apache Kafka documentation — design'
    url: 'https://kafka.apache.org/documentation/#design'
  - name: 'What do you mean by "Event-Driven"? — Martin Fowler'
    url: 'https://martinfowler.com/articles/201701-event-driven.html'
cta:
  title: '저장으로 발행하고, 쿼리로 구독합니다'
  text: 'Back4app Live Query는 엣지에서 동작하는 pub/sub입니다. 모든 클라이언트는 쿼리를 토픽 삼아 구독하고, 모든 쓰기는 일치하는 모든 구독자에게 발행됩니다. 브로커, 팬아웃, 권한은 플랫폼이 운영합니다.'
  linkText: '무료로 시작하기'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-11-02'
translationKey: pub-sub-pattern
---

**Pub/sub은 발행자가 브로커의 토픽으로 메시지를 보내고 그 토픽의 모든 구독자가 사본을 받는 메시징 패턴입니다.** 이 패턴의 가치는 전적으로 당사자들이 *모르는* 것에 있습니다. 발행자는 누가 듣고 있는지 결코 알지 못하고, 구독자는 누가 보냈는지 결코 알지 못합니다. 브로커라는 간접 계층이 이들을 공간(서로 다른 시스템), 시간(구독자가 오프라인일 수 있음), 동기화(발행은 즉시 반환됨)의 세 측면에서 분리합니다. 발행자를 건드리지 않고 구독자를 추가할 수 있다는 것, 그것이 이 패턴의 조용한 초능력입니다.

## 핵심 요약

| 질문 | 답변 |
| --- | --- |
| 등장인물 | 발행자 → 메시지 → 토픽 → 브로커 → 구독자(각자 사본을 받음) |
| vs. 큐 | 큐는 작업을 한 소비자에게 *분배*하고, pub/sub은 모두에게 *복제* |
| vs. 옵저버 | 옵저버는 프로세스 내부의 동기 방식, pub/sub은 브로커를 거치는 비동기 방식 |
| 기본 입장 | 최소 한 번 전달 + 멱등한 핸들러 |
| 약관의 세부 조항 | 응답 없음, 기본 순서 없음, 브로커를 가로지르는 원자적 트랜잭션 없음 |

## 코드로 보는 패턴

브로커가 무엇이든 모든 pub/sub API는 두 개의 동사로 환원됩니다.

```js
// 구독자: 토픽에 관심을 등록합니다
broker.subscribe('orders.paid', (msg) => fulfill(msg));   // 서비스 A
broker.subscribe('orders.paid', (msg) => notifyUser(msg)); // 서비스 B – 자신만의 사본

// 발행자: 발행 후 망각(fire and forget)
broker.publish('orders.paid', { orderId: 'o-1187' });
// 발행자는 누가 받았는지, 아니 누군가 받기는 했는지조차 알지 못합니다.
```

클라이언트 엣지에서도 형태는 같습니다. 여기서는 라이브 쿼리 구독이 토픽이고 데이터베이스 쓰기가 발행입니다.

**JavaScript:**

```javascript
// JavaScript / Node.js — Back4app JS SDK
// Pub/sub at the edge: live queries subscribe, saves publish
// Subscriber — register interest in a topic
const topic = new Parse.Query('Event');
topic.equalTo('topic', 'orders');
const sub = await topic.subscribe();
sub.on('create', (event) => handle(event.get('payload')));

// Publisher — fire and forget; no knowledge of subscribers
const event = new Parse.Object('Event');
event.set('topic', 'orders');
event.set('payload', { orderId: 'o-1187', status: 'paid' });
await event.save();
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// Pub/sub at the edge: live queries subscribe, saves publish
// Subscriber — register interest in a topic
final liveQuery = LiveQuery();
final topic = QueryBuilder<ParseObject>(ParseObject('Event'))
  ..whereEqualTo('topic', 'orders');
final sub = await liveQuery.client.subscribe(topic);
sub.on(LiveQueryEvent.create, (event) => handle(event.get('payload')));

// Publisher — fire and forget; no knowledge of subscribers
final event = ParseObject('Event')
  ..set('topic', 'orders')
  ..set('payload', {'orderId': 'o-1187', 'status': 'paid'});
await event.save();
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// Pub/sub at the edge: live queries subscribe, saves publish
// Subscriber — register interest in a topic
let topic = Event.query("topic" == "orders")
let sub = try await topic.subscribe()
sub.handleEvent { _, e in
    if case .created(let event) = e { handle(event.payload) }
}

// Publisher — fire and forget; no knowledge of subscribers
var event = Event()
event.topic = "orders"
event.payload = ["orderId": "o-1187", "status": "paid"]
try await event.save()
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// Pub/sub at the edge: live queries subscribe, saves publish
// Subscriber — register interest in a topic
val client = ParseLiveQueryClient.Factory.getClient()
val topic = ParseQuery.getQuery<ParseObject>("Event")
topic.whereEqualTo("topic", "orders")
val sub = client.subscribe(topic)
sub.handleEvent(SubscriptionHandling.Event.CREATE) { _, event ->
    handle(event.getJSONObject("payload"))
}

// Publisher — fire and forget; no knowledge of subscribers
val event = ParseObject("Event")
event.put("topic", "orders")
event.put("payload", JSONObject(mapOf("orderId" to "o-1187", "status" to "paid")))
event.save()
```

## pub/sub의 동작 방식

```mermaid
flowchart LR
  accTitle: 브로커를 거치는 발행-구독 메시지 흐름
  accDescr: 발행자가 메시지 브로커의 이름 있는 토픽으로 메시지를 보냅니다. 브로커는 토픽을 등록된 구독과 대조하고 각 구독자에게 메시지의 독립적인 사본을 전달하며, 각 구독자는 처리 완료를 따로 확인합니다.
  P["발행자<br/>(발행 후 망각)"] -->|"publish(orders.paid, msg)"| T["브로커<br/>토픽: orders.paid"]
  T -->|"사본 1"| S1["구독자 A<br/>주문 처리"]
  T -->|"사본 2"| S2["구독자 B<br/>알림"]
  T -->|"사본 3"| S3["구독자 C<br/>분석"]
```

브로커는 네 가지 일을 합니다. 발행을 **수락**하고 즉시 반환합니다. 각 메시지를 구독과 **매칭**합니다(토픽 이름으로, `orders.*`처럼 계층적으로, 또는 콘텐츠 속성으로). 구독자마다 독립적인 사본을 **전달**하며 보장 수준에 따라 재시도합니다. 그리고 내구성이 설정된 경우 오프라인 구독자를 위해 메시지를 **보존**합니다. 정오에 배포된 서비스가 그동안 놓친 것을 따라잡을 수 있게 해 주는 시간적 분리입니다. 짚어 둘 만한 용어가 하나 있습니다. *메시지*는 봉투이고, pub/sub이 보통 실어 나르는 것은 *이벤트*("이 일이 일어났음"이라는 과거형 사실)입니다. 이벤트가 사실만 담는지 전체 상태를 담는지는 [Fowler](https://martinfowler.com/articles/201701-event-driven.html)가 말한 이벤트 알림 vs. 이벤트 전달 상태 전송의 구분입니다.

## Pub/sub vs. 메시지 큐 vs. 옵저버 패턴

두 가지 혼동을 표 하나로 정리합니다.

| | Pub/sub | 메시지 큐 | 옵저버 |
| --- | --- | --- | --- |
| 전달 | 모든 구독자가 사본을 받음 | 각 메시지를 정확히 하나의 워커가 소비 | 주체가 각 옵저버를 호출 |
| 목적 | 이벤트 방송 | 작업 분배 | 프로세스 내부에서 상태에 반응 |
| 결합 | 없음, 브로커가 중개 | 없음, 큐가 중개 | 직접 객체 참조 |
| 타이밍 | 비동기 | 비동기 | 보통 동기 |
| 경계 | 프로세스/시스템 간 | 워커 간 | 단일 프로세스 내부 |
| 전형적인 형태 | 이벤트 1개 → 반응 N개 | 작업 N개 → 워커 M개 | 객체 1개 → 그 리스너들 |

회의가 끝나도 살아남는 한마디는 이것입니다. **큐는 작업을 분배하고, pub/sub은 작업을 복제합니다.** 그리고 옵저버는 "작은 pub/sub"이 아닙니다. 브로커를 통한 간접성이야말로 옵저버에 없는 것이며, 그래서 옵저버를 pub/sub으로 리팩터링하는 일은 이름 바꾸기가 아니라 아키텍처 변경입니다. 실제로 브로커들은 두 형태를 모두 제공합니다. 소비자 그룹은 토픽을 그룹별 큐로 바꾸고([Kafka](https://kafka.apache.org/documentation/#design)의 대표적인 방식), 팬아웃 익스체인지는 큐 시스템을 pub/sub으로 바꿉니다.

## 전달 보장: 최대 한 번 vs. 최소 한 번 vs. 정확히 한 번

| 보장 | 장애 양상 | 비용 | 핸들러가 해야 할 일 | 선택할 때 |
| --- | --- | --- | --- | --- |
| 최대 한 번 (at-most-once) | 메시지가 사라질 수 있음 | 가장 저렴하고 빠름 | 누락을 견딤 | 휘발성 데이터: 프레즌스, 시세, 메트릭 |
| 최소 한 번 (at-least-once) | 중복 도착 | ack + 재시도 | **멱등해야 함** | 비즈니스 이벤트의 기본값 |
| 정확히 한 번 (exactly-once) | 제약이 많고 복잡 | 중복 제거 + 조정 | 그래도 멱등해야 함 | 브로커가 지원하는 좁은 경로 |

[MQTT](https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html)의 QoS 레벨 0/1/2는 이 세 가지에 정확히 대응합니다. 이 삼분법이 벤더 마케팅이 아니라 근본적인 구분임을 깔끔하게 확인해 주는 사실입니다. 엔지니어링의 진실은 이렇습니다. 종단 간 정확히 한 번은 일반적으로 달성할 수 없습니다(브로커 배지를 단 두 장군 문제입니다). 이를 내세우는 시스템은 최소 한 번 전달과 중복 제거를 결합한 것입니다. 그래서 이 패턴의 실제 계약은 핸들러에서 구현됩니다. **최소 한 번 + 멱등성**: 메시지 ID로 중복을 제거하거나, 엔티티를 키로 한 upsert를 쓰거나, 적용 전에 버전을 확인하면 중복은 버그가 아니라 아무 일도 하지 않는 작업(no-op)이 됩니다.

## 순서, 데드 레터, 토픽 설계

입문용 다이어그램이 건너뛰는 세 가지 운영 현실입니다. **순서:** 병렬 소비자로의 팬아웃에는 전역 순서가 없습니다. 브로커는 병렬성을 대가로 파티션이나 순서 키 단위로 순서를 복원합니다. 도착 순서가 아니라 엔티티 ID와 버전에 따라 이벤트를 적용하도록 핸들러를 설계하면 문제는 대부분 사라집니다. **포이즌 메시지:** 처리할 때마다 예외를 던지는 메시지는 최소 한 번 방식에서 영원히 재시도됩니다. 전달 시도 횟수를 제한하고 실패한 메시지를 분류할 수 있도록 **데드 레터 큐**로 보내야 합니다. 그렇지 않으면 잘못된 이벤트 하나가 토픽 전체를 멈춰 세웁니다. **토픽 설계:** 이름은 계층적으로 짓고(`orders.paid`, `orders.refunded`, 와일드카드 `orders.*`), 세분성은 구독자들이 실제로 갈리는 수준에 맞추며, 메시지 스키마는 추가 방식으로 진화시켜야 합니다. 구독자는 모르는 필드를 무시하고, 호환성을 깨는 변경은 API 버전과 똑같이 마이그레이션 기간을 둔 새 버전 토픽(`orders.v2`)으로 내보냅니다.

## 일반적인 사용 사례

- **마이크로서비스 통합**: 서로 다른 팀이 소유한 서비스가 직접 호출이나 공유 배포 일정 없이 서로의 [이벤트](/glossary/ko/decoupled-architecture/)에 반응합니다.
- **실시간 클라이언트 기능**: 채팅 팬아웃, 실시간 대시보드, [프레즌스](/glossary/ko/presence-online-status/). 엣지에서 웹소켓 위로 동작하는 pub/sub입니다.
- **IoT 텔레메트리**: 수천 대의 디바이스가 MQTT로 발행하고, 알림부터 분석까지 여러 소비자가 독립적으로 구독합니다.
- **캐시 무효화와 복제**: 쓰기 하나를 발행하면 모든 캐시와 복제본이 통보받습니다.
- **병렬 처리 파이프라인**: 하나의 이벤트가 주문 처리, 알림, 분석을 동시에 촉발하고, 각각은 자기 속도로 진행합니다.

## pub/sub을 사용해야 할까? 결정 매트릭스

| pub/sub이 맞는 경우 | 다른 방식을 택하는 것이 좋은 경우 |
| --- | --- |
| 하나의 이벤트에 여러 독립 소비자가 관심을 가짐 | 소비자가 하나: 큐, 또는 그냥 호출 |
| 생산자와 소비자가 독립적으로 배포됨 | 호출자가 지금 답을 필요로 함 → 요청-응답 |
| 최종 일관성을 받아들일 수 있음 | 당사자 간 원자성이 필요함 → 트랜잭션/사가 |
| 구독자가 시간에 따라 생기고 사라짐 | 엄격한 전역 순서를 양보할 수 없음 |
| 시스템 사이에서 급증하는 부하를 완충해야 함 | 시스템 전체가 한 프로세스에 들어감 → 옵저버 |

## 한계와 트레이드오프

- **디버깅이 브로커를 건너야 합니다.** "누가 이것을 소비했고 왜 실패했는가?"에 답하려면 상관관계 ID와 트레이싱이 필요합니다. 팀을 자유롭게 하는 디커플링이 인과관계도 숨깁니다.
- **설계상 응답이 없습니다.** 응답이 필요한 워크플로는 이를 추가 메시지로 모델링해야 합니다. 함수 호출 하나로 충분했던 곳에 지연 시간과 상태 머신이 더해집니다.
- **브로커는 인프라입니다.** 메시징 계층의 가용성, 용량, 보안, 보존 정책이 플랫폼 작업이 됩니다. 브로커가 다운되면 장애가 몇 배로 커집니다.
- **발행자의 무지는 양날의 검입니다.** 발행자는 구독자의 상태를 관찰할 수 없습니다. 조용히 실패하는 소비자는 데드 레터 모니터링이 없다면 데이터를 잃습니다.
- **보장은 처리량을 대가로 합니다.** 순서 키는 직렬화하고, 정확히 한 번은 조정하며, 내구성은 영속화합니다. 의미론을 강화할 때마다 성능을 씁니다. 데이터에 꼭 필요한 보장만 선택하는 것이 좋습니다.

## Back4app에서의 pub/sub

Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. 이 패턴은 여기서 클라이언트 엣지에 나타납니다. [Live Query](/glossary/ko/real-time-live-queries/)는 *쿼리가 곧 토픽*인 pub/sub입니다. 구독자가 조건(predicate)을 등록하면 그 조건에 맞는 모든 쓰기가 관리형 [웹소켓](/glossary/ko/websockets-real-time-sync/) 연결을 통해 모든 구독자에게 발행되며, 위의 코드 탭처럼 권한은 구독자별로 검사되고 팬아웃은 플랫폼이 처리합니다. 서버 측 반응도 같은 방식으로 구성됩니다. Cloud Code 트리거는 데이터 변경을 구독하고(`afterSave`는 사실상 토픽 구독입니다), 예약 함수는 행으로 발행된 작업을 처리합니다. 대규모 서비스 간 스트리밍에는 여전히 Kafka나 RabbitMQ 같은 전용 브로커가 필요합니다. 하지만 일반적인 제품 사례에서는 변경 시 방송하는 기능이 백엔드와 함께 제공됩니다.
