1. Queue와 Log를 한 번에 구분하는 방법

헷갈리면 이것만 기억하면 됩니다.

구분QueueEvent Log
핵심 질문누가 이 일을 할 것인가?무슨 일이 발생했는가?
소비 방식경쟁 소비독립 소비
하나의 Event보통 한 Consumer가 처리여러 Consumer가 읽을 수 있음
readVisibility 상태 변경 가능단순 조회
처리 상태Visibility TimeoutCursor / Offset
완료delete/archiveLog 유지 가능
적합한 용도작업 분배이벤트 전파
대표 구조PGMQ, SQS 계열Kafka/Event Store 계열

한 줄로 줄이면:

Queue = Work Distribution

Log = Event Distribution

조금 더 직관적으로는:

Queue
"이 일 누가 할래?"

Log
"이런 일이 일어났으니 필요한 사람은 읽어."

이 차이가 큽니다.


2. PGMQ 설명과 후반

Log 설명은 어떻게 연결되는가?

초반 워크플로에서는 다음처럼 설명됩니다.

PGMQ
 ↓
Event Broker
 ↓
Client

즉 Broker 역시 PGMQ에서 결과를 가져오는 것처럼 보입니다.

그런데 후반의 Queue 읽기와 Log 읽기에서는 Broker가 여러 대로 늘어났을 때
경쟁 소비 때문에 발생하는 문제를 설명하면서
Immutable Event Log + Cursor 방식을 제시합니다.

따라서 개념적으로 다음처럼 나누어 이해하면 훨씬 깔끔합니다.

요청 / 작업 분배
      ↓
    Queue
      ↓
    Worker


결과 / 이벤트 전파
      ↓
 Event Log
      ↓
   Brokers

즉 아키텍처가 성장하면서 역할이 더 명확하게 분리되는 것입니다.


3. 전체 아키텍처를 다시 그려보면

최종적으로는 다음과 같은 모습으로 이해할 수 있습니다.

┌────────────────┐
│     Client     │
└───────┬────────┘
        │
        │ WebSocket
        ▼
┌────────────────┐
│  Event Broker  │
└───────┬────────┘
        │
        │ Command Event
        ▼
┌────────────────┐
│      PGMQ      │
│   Work Queue   │
└───────┬────────┘
        │
        │ read
        ▼
┌────────────────┐
│     Worker     │
└───────┬────────┘
        │
        ├──────────── DB
        │
        ├──────────── LLM
        │
        ├──────────── RAG
        │
        ├──────────── Tool
        │
        └──────────── External API
        │
        │ Result Event
        ▼
┌────────────────┐
│   Event Store  │
│      LOG       │
└───────┬────────┘
        │
        │ tail
   ┌────┼────────────┐
   │    │            │
   ▼    ▼            ▼
Broker A          Broker B          Broker C
   │                 │                 │
   ▼                 ▼                 ▼
Clients           Clients           Clients

역할이 아주 명확합니다.

Client
= 무엇을 원하는가?

Broker
= 이벤트를 어디로 전달할 것인가?

Queue
= 누가 이 일을 처리할 것인가?

Worker
= 실제로 무엇을 처리할 것인가?

Event Log
= 무슨 일이 발생했는가?

4. AI Agent에 이 구조가

잘 맞는 이유

이 아키텍처가 특히 AI Agent와 잘 맞는 이유가 있습니다.

AI 작업은 보통 한 번의 함수 호출로 끝나지 않습니다.

사용자 요청
   ↓
LLM 추론
   ↓
Tool 호출
   ↓
Tool 결과
   ↓
LLM 재추론
   ↓
파일 수정
   ↓
Shell 실행
   ↓
LLM 재추론
   ↓
최종 결과

처리 시간이 굉장히 길어질 수 있습니다.

여기에 스트리밍까지 필요합니다.

"분석하고 있습니다..."

"파일을 찾았습니다."

"테스트를 실행합니다."

"테스트 성공"

"코드를 수정합니다."

기존 HTTP Request 하나로 이 전체 수명주기를 관리하면 복잡해집니다.

Event 기반이라면 각 상태를 Event로 표현할 수 있습니다.

{
  "type": "TOOL_STARTED",
  "tool": "bash"
}
{
  "type": "TOOL_OUTPUT",
  "data": "BUILD SUCCESS"
}
{
  "type": "LLM_STREAM",
  "data": "현재 코드를 분석하면..."
}
{
  "type": "TASK_COMPLETED"
}

Worker는 Client가 어디 있는지 몰라도 됩니다.

그냥

Event 생성
     ↓
Event Store / Queue 등록

만 하면 됩니다.

나머지는 Broker가 처리합니다.


5. LLM Streaming도 결국 Event다

예를 들어 Worker가 LM Studio와 통신하고 있다고 가정합니다.

LM Studio
   │
   │ Token Stream
   ▼
Worker

Worker에게 다음과 같은 데이터가 계속 들어옵니다.

"Spring"
" Boot"
" 기반으로"
" 구현하면"
"..."

Worker는 이를 클라이언트에게 직접 보내려고 고민할 필요가 없습니다.

각 Chunk를 Event로 바꿉니다.

LLM Stream
     ↓
Event
     ↓
Event
     ↓
Event
     ↓
Broker
     ↓
Client

Bash 실행도 마찬가지입니다.

Bash Process

stdout
stderr
  │
  ▼
Worker
  │
  │ Event
  ▼
Broker
  │
  ▼
Client

강의에서는 Bash의 stdout이나 LLM의 streaming 결과가
발생할 때마다 이를 Event로 감싸 전송하고,
Broker가 해당 이벤트를 원하는 Client에게 전달하는 구조로 설명합니다.
Worker는 Client와의 통신 방식에 신경 쓰지 않고 단순히 Event만 발생시키면 됩니다.

이게 굉장히 중요한 장점입니다.

Worker가 모르는 것

Client IP
WebSocket Session
Client 연결 서버
Client UI
렌더링 방식
네트워크 처리

Worker가 알아야 하는 건:

"무슨 Event를 발생시킬 것인가?"

정도입니다.

결합도가 크게 낮아집니다.


6. Spring으로 옮겨보면

어떻게 될까?

강의 구현은 Express 기반이지만 개념 자체는 특정 언어나 프레임워크에 종속되지 않습니다.

Spring으로 옮겨보면 다음과 같이 생각할 수 있습니다.

기존 방식은:

@PostMapping("/summary")
public SummaryResponse summary(
        @RequestBody SummaryRequest request
) {

    Consultation consultation =
        consultationRepository.findById(request.id())
            .orElseThrow();

    String summary =
        llmService.summarize(consultation);

    summaryRepository.save(
        new Summary(request.id(), summary)
    );

    return new SummaryResponse(summary);
}

LLM 호출이 끝날 때까지 요청이 유지됩니다.

비동기 Event 구조라면

@PostMapping("/summary")
public ResponseEntity<AcceptedResponse> summary(
        @RequestBody SummaryRequest request
) {

    String taskId = UUID.randomUUID().toString();

    eventQueue.write(
        new SummaryRequested(
            taskId,
            request.id()
        )
    );

    return ResponseEntity
        .accepted()
        .body(new AcceptedResponse(taskId));
}

API는 빠르게 끝납니다.

HTTP Request
     ↓
Queue write
     ↓
202 Accepted

Worker가 별도로 처리합니다.

public void handle(SummaryRequested event) {

    Consultation consultation =
        consultationRepository
            .findById(event.consultationId())
            .orElseThrow();

    String summary =
        llmService.summarize(consultation);

    summaryRepository.save(
        new Summary(
            event.consultationId(),
            summary
        )
    );

    eventPublisher.publish(
        new SummaryCompleted(
            event.taskId(),
            summary
        )
    );
}

클라이언트는 WebSocket으로 결과를 받을 수 있습니다.

Browser
   │
   │ WebSocket
   ▼
Spring Broker
   │
   │ SUMMARY_COMPLETED
   ▼
Browser UI 갱신

강의의 구조를 그대로 따르려면 WebSocket 같은 양방향 소켓이 가장 자연스럽습니다.
다만 실제 Spring 웹서비스에서는
요청은 HTTP로 보내고 결과 스트림만 SSE로 받는 구조로 변형할 수도 있습니다.


7. 이 구조가 확장에 강한 이유

결국 핵심은 분리입니다.

접속 처리
     ↓
Event Broker


작업 보관
     ↓
Queue


비즈니스 처리
     ↓
Worker


이벤트 기록
     ↓
Event Log


화면 처리
     ↓
Client

각각 따로 확장할 수 있습니다.

Worker만 부족하다면

Worker 1
Worker 2
Worker 3
Worker 4

Broker가 부족하다면

Broker 1
Broker 2
Broker 3

Queue가 한계에 도달한다면

PGMQ AI
PGMQ File
PGMQ Notification

처럼 나눌 수도 있습니다.

새 기능이 생겼다고 Broker 전체를 수정할 필요도 없습니다.

새 Event Type 추가

        +

새 Worker 추가

정도로 해결할 수 있습니다.


8. 그렇다고 모든 시스템을 비동기로 만들 필요는 없다

비동기가 무조건 좋은 것은 아닙니다.

예를 들어 단순 회원 조회가

SELECT
 ↓
50ms
 ↓
Response

에 끝난다면 굳이

Request
 ↓
Queue
 ↓
Worker
 ↓
Event
 ↓
Broker
 ↓
Client

로 만들 이유가 없습니다.

오히려 복잡도만 커집니다.

반대로 다음과 같은 작업은 비동기화의 효과가 큽니다.

LLM / AI Agent
RAG
파일 분석
이미지·영상 처리
메일·알림 발송
외부 API 연계
대규모 데이터 처리
배치 작업
오래 걸리는 Tool 실행

즉 핵심은

오래 걸리고, 실패할 수 있으며, 재시도가 필요하고, 여러 작업을 병렬로 처리해야 하는 곳을 비동기화하는 것입니다.


마무리

이번 아키텍처의 핵심을 한 문장으로 정리하면 다음과 같습니다.

클라이언트 요청을 즉시 처리하지 않고 Queue에 저장한 뒤
Worker가 비동기로 처리하고, 처리 과정과 결과를 Event로 만들어
Broker를 통해 클라이언트에게 전달하는 구조입니다.

그리고 시스템 규모가 커지면 역할을 조금 더 명확하게 나눕니다.

Command / Job

Client
   ↓
Broker
   ↓
Queue
   ↓
Worker

그리고 결과는:

Event

Worker
   ↓
Event Log
   ↓
Broker
   ↓
Client

로 흐르게 됩니다.

결국 이번 내용을 이해할 때 가장 중요한 흐름은 이것입니다.

동기 Request/Response
        ↓

요청과 처리를 분리

        ↓

Event
        ↓
Queue
        ↓
Worker

        ↓

Visibility Timeout
Retry
Idempotency

        ↓

Result Event
        ↓
Event Log
        ↓
Cursor / Offset
        ↓
Fan-out
        ↓
Event Broker
        ↓
Client

그리고 Queue와 Log가 헷갈릴 때는 딱 두 문장만 기억하면 됩니다.

Queue는 "누가 이 일을 할 것인가?"를 해결합니다.
Log는 "무슨 일이 발생했는가?"를 여러 Consumer에게 전달합니다.

이 두 가지를 구분해서 이해하면 PGMQ, Worker, Event Broker, Visibility Timeout, Fan-out, Cursor 같은 개념들이 각각 따로 노는 것이 아니라
하나의 비동기 이벤트 아키텍처 안에서 왜 필요한지 자연스럽게 연결됩니다.

profile
레거시를 이해하면서도 새로운 기술을 현실적으로 적용할 수 있는 백엔드 개발자가 되는 것이 목표입니다.

0개의 댓글