이벤트 기반 AI 코딩 에이전트 아키텍처와 유지보수 가능한 바이브 코딩

AI 코딩 에이전트를 단순히 LLM API를 호출하는 프로그램으로 생각하면 구조는 어렵지 않습니다.

사용자가 요청을 입력하고, LLM에 프롬프트를 전달하고, 결과를 화면에 출력하면 됩니다.

사용자
  ↓
웹 애플리케이션
  ↓
LLM API
  ↓
응답

하지만 실제 기업 환경에서 사용하는 AI 에이전트는 이렇게 단순하지 않습니다.

하나의 요청 안에서도 모델 추론, 도구 실행, 여러 차례의 반복 추론, 스트리밍 응답, 세션 관리, 프로젝트 관리, 조직 정책 적용, MCP 연결, 스킬 로딩, 로그 기록 등이 동시에 발생합니다.

이 모든 기능을 하나의 서버와 하나의 긴 실행 흐름으로 처리하기 시작하면 시간이 지날수록 구조가 복잡해집니다.

그래서 AI 에이전트 역시 기존 백엔드 시스템과 마찬가지로 역할을 분리하고, 이벤트를 중심으로 각 컴포넌트가 느슨하게 연결되는 구조를 고려할 필요가 있습니다.


단순한 AI 호출을 넘어 하나의 시스템으로 바라보기

전체 구조를 단순화하면 다음과 같이 볼 수 있습니다.

Browser
   ↓
Web Client
   ↓
Express Server
   ├─ Backend API
   ├─ Event Broker
   └─ WebSocket
          ↓
      PostgreSQL
      ├─ Business Table
      └─ PGMQ
          ↓
 ┌────────┼────────────┬─────────────┐
 ↓        ↓            ↓             ↓
Turn     Tool       Inference     Read Model
Worker   Worker      Worker        Worker

PostgreSQL은 일반적인 비즈니스 데이터 저장소 역할뿐 아니라 이벤트 전달을 위한 큐의 기반으로도 활용할 수 있습니다.

웹 서버는 외부 요청을 받아들이고 WebSocket을 통해 클라이언트와 통신하며, 실제 시간이 오래 걸리는 작업은 워커가 처리합니다.

이 구조에서 중요한 것은 모든 기능을 하나의 서버 프로세스가 직접 수행하지 않는다는 점입니다.

웹 서버는 요청을 받고 이벤트를 발생시키며, 각 워커는 자신이 처리해야 할 이벤트만 가져가 처리합니다.


왜 워커를 여러 개로 분리하는가

AI 에이전트에는 성격이 전혀 다른 작업들이 존재합니다.

예를 들어 모델 추론은 몇 초에서 수십 초 이상 걸릴 수 있습니다.

반면 다음 상태를 결정하거나 이벤트를 하나 발행하는 작업은 매우 짧게 끝납니다.

이 두 작업을 같은 실행 흐름에서 처리하면 긴 작업이 시스템 전체 흐름을 붙잡게 됩니다.

그래서 역할에 따라 워커를 분리할 수 있습니다.

Worker주요 역할
Turn Worker하나의 Turn 실행 흐름과 상태 제어
Tool WorkerBash, MCP 등 외부 도구 실행
Inference WorkerLLM 호출과 추론 처리
Read Model Worker이벤트를 조회용 데이터로 변환
Event Broker이벤트를 WebSocket 클라이언트로 전달

특히 Inference Worker를 별도로 분리하는 이유는 명확합니다.

LLM 추론은 시스템에서 가장 오래 걸리는 작업 중 하나이기 때문입니다.

Turn Worker
    ↓
Inference 요청 Event
    ↓
Inference Worker
    ↓
LLM
    ↓
Model Reply Event

Turn Worker가 LLM 응답을 기다리면서 계속 실행 상태를 유지할 필요가 없습니다.

이벤트만 남긴 후 자신의 작업을 종료하면 됩니다.

추론이 끝나면 Inference Worker가 새로운 이벤트를 발행하고, Turn Worker가 다시 해당 이벤트를 가져가 다음 단계를 진행합니다.

이러한 방식은 긴 생명주기를 가진 하나의 스레드를 유지하기보다는 짧은 작업을 반복해서 처리하는 형태에 가깝습니다.


하나의 Turn도 이벤트의 연속으로 처리할 수 있다

사용자가 다음과 같은 요청을 입력했다고 가정하겠습니다.

현재 프로젝트의 배포 가이드를 작성해 주세요.

전통적인 방식이라면 하나의 함수 내부에서 모든 처리가 진행될 수 있습니다.

요청 수신
→ LLM 호출
→ Tool 호출
→ Tool 결과
→ LLM 재호출
→ 최종 응답

이벤트 기반 구조에서는 이를 여러 단계로 나눌 수 있습니다.

Turn Requested
      ↓
Turn Started
      ↓
Model Requested
      ↓
Model Replied
      ↓
Decision
      ↓
Tool Requested
      ↓
Tool Completed
      ↓
Model Requested
      ↓
Model Replied
      ↓
Turn Final

각 단계는 이전 단계의 결과를 이벤트로 전달받습니다.

예를 들어 모델이 Bash 명령 실행이 필요하다고 판단하면 Tool Requested 이벤트가 생성됩니다.

Tool Worker는 해당 이벤트를 가져와 명령을 실행합니다.

Tool Requested
      ↓
Tool Worker
      ↓
Bash 실행
      ↓
Tool Completed

그리고 Tool Completed 이벤트를 Turn Worker가 다시 가져갑니다.

Tool 결과까지 포함하여 새로운 모델 입력을 구성하고 다시 Model Requested 이벤트를 발행합니다.

이 과정이 반복됩니다.

중요한 점은 중앙에서 무한 루프 하나가 전체 프로세스를 계속 붙잡고 있는 구조가 아니라는 것입니다.

Event
 ↓
Worker
 ↓
Event
 ↓
Worker
 ↓
Event

각 Worker는 자신이 관심 있는 이벤트를 처리하고 다음 이벤트를 발행합니다.


이벤트 소싱과 Read Model

이벤트 소싱에서는 시스템에서 발생한 사건을 이벤트 형태로 기록합니다.

예를 들어 하나의 대화에서도 다음과 같은 이벤트가 계속 발생할 수 있습니다.

SessionOpened
TurnStarted
ModelRequested
ModelReplied
ToolRequested
ToolCompleted
TurnCompleted

문제는 이런 원시 이벤트만 가지고 화면을 구성하기 어렵다는 것입니다.

채팅 화면에서는 일반적으로 다음과 같은 정보가 필요합니다.

세션
 ├─ 사용자 메시지
 ├─ AI 메시지
 ├─ 실행 상태
 ├─ Tool 실행 결과
 └─ 최종 응답

이벤트를 매번 처음부터 분석해서 화면을 구성하는 것은 비효율적입니다.

그래서 Read Model을 만듭니다.

Event Store / Queue
        ↓
Read Model Worker
        ↓
Projection
        ↓
Session Table
Chat Table
Turn Table

Read Model Worker는 이벤트를 읽은 뒤 화면이나 조회 API에서 사용하기 좋은 형태로 데이터를 가공합니다.

따라서 클라이언트가 과거 대화 내용을 조회할 때는 정리된 테이블을 사용하고, 현재 진행 중인 모델 응답은 WebSocket 이벤트를 통해 실시간으로 받을 수 있습니다.

과거 데이터
DB → API → Client

실시간 데이터
Worker → Event Broker → WebSocket → Client

두 방식을 함께 사용하는 것입니다.


WebSocket과 Event Broker의 역할

LLM 응답은 한 번에 전달하기보다 스트리밍 형태로 전달하는 경우가 많습니다.

Inference Worker가 모델에서 생성되는 토큰이나 Chunk를 받으면 이를 이벤트로 전달합니다.

LLM
 ↓
Inference Worker
 ↓
Model Stream Chunk
 ↓
PGMQ
 ↓
Event Broker
 ↓
WebSocket
 ↓
Browser

브라우저 입장에서는 AI가 실시간으로 문장을 작성하는 것처럼 보입니다.

하지만 내부적으로는 여러 컴포넌트를 거쳐 이벤트가 전달되고 있는 것입니다.

Event Broker는 클라이언트가 구독한 이벤트 종류를 확인하고 필요한 메시지만 WebSocket을 통해 전달하는 역할을 수행할 수 있습니다.


PGMQ를 사용할 때 멱등성이 중요한 이유

이벤트 기반 시스템에서는 동일한 메시지가 다시 전달될 가능성을 항상 고려해야 합니다.

중요한 것은 특정 Queue 제품을 사용하는지보다 소비자가 동일한 이벤트를 여러 번 받더라도 결과가 달라지지 않게 만드는 것입니다.

예를 들어 다음 이벤트가 있다고 가정하겠습니다.

{
  "eventId": "EVT-10001",
  "type": "PAYMENT_COMPLETED"
}

같은 이벤트가 두 번 전달되더라도 실제 비즈니스 처리는 한 번만 이루어져야 합니다.

EVT-10001 수신
      ↓
처리 이력 조회
      ↓
처음 수신
      ↓
비즈니스 처리
      ↓
처리 완료 기록

다시 같은 이벤트가 들어온다면 다음과 같이 처리할 수 있습니다.

EVT-10001 재수신
      ↓
이미 처리됨
      ↓
Skip

Redis나 별도의 Idempotency Table 등을 이용할 수 있습니다.

idempotency_key
status
created_at
completed_at

AI 에이전트에서도 Tool 호출과 같은 작업에는 특히 중요합니다.

단순 조회 명령이 두 번 실행되는 것은 큰 문제가 아닐 수 있지만 결제, 이메일 전송, 파일 삭제, 외부 API 실행 같은 작업이 중복 실행되면 실제 장애로 이어질 수 있기 때문입니다.


세션 초기화도 하나의 중요한 워크플로다

AI 에이전트에서 세션을 만드는 것은 단순히 채팅방 하나를 생성하는 작업으로 끝나지 않을 수 있습니다.

기업용 코딩 에이전트라면 다음과 같은 초기화 과정이 필요할 수 있습니다.

Session Open
      ↓
Project 확인
      ↓
Workspace 생성
      ↓
Organization 확인
      ↓
Policy 병합
      ↓
Skill Index 생성
      ↓
MCP Binding
      ↓
Model 결정
      ↓
Context 구성

특히 프로젝트와 조직의 연결은 기업 환경에서 중요합니다.

개인이 마음대로 사용할 모델이나 도구를 선택하는 것보다 조직 정책에 따라 사용할 수 있는 자원을 제한해야 하는 경우가 있기 때문입니다.


조직 계층과 정책 상속

기업에는 조직 계층이 존재합니다.

Company
  ↓
Division
  ↓
Team
  ↓
Project
  ↓
User

각 조직에서 사용할 모델, Skill, MCP, 정책을 설정할 수 있습니다.

하위 조직에 별도 설정이 없다면 상위 조직의 설정을 상속받는 방식으로 구성할 수 있습니다.

예를 들어 모델 선택 정책은 다음처럼 동작할 수 있습니다.

Project Organization Model
        ↓ 없으면
Parent Organization Model
        ↓ 없으면
Default Model

Skill 역시 충돌 정책이 필요합니다.

예를 들어 같은 이름의 Skill이 여러 곳에 존재한다면 다음과 같은 우선순위를 설계할 수 있습니다.

Organization Policy
        ↓
Project Skill
        ↓
User Global Skill

중요한 것은 어느 우선순위가 반드시 정답이라는 것이 아니라, 정책 충돌이 발생했을 때 어떤 설정이 최종적으로 적용되는지가 시스템 전체에서 일관되어야 한다는 점입니다.


MCP 도구를 무작정 Context에 넣으면 문제가 생긴다

MCP 서버를 많이 연결하면 사용할 수 있는 Tool도 증가합니다.

문제는 모든 Tool 정의를 매번 모델 Context에 포함하면 Context가 지나치게 커질 수 있다는 것입니다.

MCP Server A
 ├─ Tool 1
 ├─ Tool 2
 └─ Tool 3

MCP Server B
 ├─ Tool 4
 ├─ Tool 5
 └─ Tool 6

서버가 계속 추가되면 Tool 정의 역시 계속 증가합니다.

이를 해결하는 방법 중 하나는 Tool을 직접 모두 노출하는 대신 필요한 기능을 Skill 형태로 묶어 필요할 때 로딩하는 구조입니다.

Model
 ↓
Skill 선택
 ↓
MCP Skill Load
 ↓
필요한 MCP Tool 확인
 ↓
Tool 실행

결국 중요한 것은 모델에게 가능한 모든 정보를 처음부터 제공하는 것이 아니라 현재 작업에 필요한 정보만 적절한 시점에 제공하는 것입니다.


Context가 크다고 무조건 좋은 것은 아니다

AI 에이전트를 개발하다 보면 Context Window가 큰 모델이 무조건 좋아 보일 수 있습니다.

하지만 Context가 커지면 처리해야 하는 KV Cache의 양도 커지고 메모리 사용량과 추론 비용이 증가합니다.

따라서 모델의 성능을 단순히 다음과 같이 비교해서는 안 됩니다.

100 TPS
80 TPS
60 TPS

실제 환경에서는 어느 정도의 Context를 사용했는지 함께 확인해야 합니다.

Model A
Context 4K
TPS 100

Model B
Context 128K
TPS 30

두 숫자를 그대로 비교하면 의미가 없습니다.

AI 에이전트처럼 긴 세션을 유지해야 하는 시스템에서는 실제 운영과 비슷한 Context 크기와 동시 요청 조건에서 측정해야 합니다.


Prefix Cache가 에이전트 성능에 미치는 영향

여러 차례 대화를 진행하는 AI Agent에서는 이전 Context가 계속 반복됩니다.

System Prompt
Policy
Skills
Conversation History
Previous Tool Results
New Prompt

모든 요청에서 전체 Context를 처음부터 다시 계산한다면 비용이 상당히 커집니다.

Prefix Cache가 제대로 작동하면 이전에 계산한 부분을 재사용하고 새로 추가된 부분만 처리할 수 있습니다.

이전 Context
[ Cached Prefix ]

새로운 Prompt
[ New Tokens ]

        ↓

Cached Prefix 재사용
+
New Tokens만 Prefill

따라서 장시간 동작하는 에이전트에서는 단순 TPS만큼 Prefix Cache가 안정적으로 유지되는지도 중요합니다.

Prompt 구조가 계속 변경되면서 Prefix가 깨지면 매번 긴 Context를 다시 계산해야 하기 때문입니다.

AI 에이전트의 성능 최적화는 결국 모델 속도만의 문제가 아닙니다.

Model Performance
+
Context Strategy
+
Prefix Cache
+
Batch Strategy
+
Worker Architecture
+
Streaming

여러 요소를 함께 봐야 합니다.


AI 코딩에서 더 중요한 것은 코드 생성이 아니다

AI 코딩 도구가 발전하면서 기능 하나를 만들어내는 것 자체는 점점 쉬워지고 있습니다.

문제는 그 다음입니다.

처음 만들어진 코드는 동작할 수 있습니다.

하지만 기능을 계속 추가하면 상황이 달라집니다.

기능 추가
 ↓
조건 추가
 ↓
예외 처리 추가
 ↓
새로운 API 추가
 ↓
다른 Worker 추가
 ↓
MCP 추가
 ↓
새로운 Event 추가

초기의 작은 구조가 빠르게 복잡해집니다.

이때 설계 기준이 없으면 AI 역시 기존 코드 위에 계속 코드를 덧붙입니다.

결과적으로 코드는 실행되지만 유지보수하기 어려운 구조가 만들어질 수 있습니다.


바이브 코딩은 팀원에게 일을 맡기는 것과 비슷하다

AI를 활용한 개발에서 개발자의 역할은 점점 코드를 직접 입력하는 사람보다 설계하고 검증하는 사람에 가까워지고 있습니다.

과거에는 다음 과정 대부분을 개발자가 직접 수행했습니다.

요구사항 분석
→ 설계
→ 구현
→ 테스트
→ 디버깅
→ 리팩토링

AI를 적극적으로 사용하는 환경에서는 일부 구현을 AI에게 위임할 수 있습니다.

요구사항 분석
→ 아키텍처 설계
→ 작업 분해
→ AI 구현
→ 코드 리뷰
→ 테스트
→ 구조 검증
→ 리팩토링

그러나 설계와 검증까지 AI에게 모두 맡기는 것은 아직 위험합니다.

AI는 기능을 빠르게 구현하는 능력은 뛰어나지만 장기적인 변경 가능성, 도메인 경계, 일관된 네이밍, 컴포넌트 역할까지 항상 완벽하게 유지해 주지는 않습니다.

그래서 개발자가 계속 질문해야 합니다.

이 클래스가 왜 여기에 있는가?

이 역할은 이 도메인의 책임이 맞는가?

Turn Worker가 해야 할 일인가?

Inference Worker가 해야 할 일인가?

이 Event 이름이 도메인 언어와 일치하는가?

비슷한 코드가 다른 곳에도 존재하지 않는가?

이 구조에서 Worker가 추가되면 어떻게 확장되는가?

MCP가 추가되면 현재 설계가 유지되는가?

AI에게 코드를 만들어 달라고 요청하는 것보다 이런 질문을 던질 수 있는 능력이 더 중요해지고 있습니다.


Ubiquitous Language도 여전히 중요하다

AI가 코드를 작성한다고 해서 기본 원칙이 사라지는 것도 아닙니다.

예를 들어 시스템에서는 Turn Worker라고 부르고 있는데 코드에서는 같은 역할을 Reactor, Processor, Handler 등 서로 다른 이름으로 표현하기 시작하면 문제가 발생합니다.

사람과 AI가 같은 개념을 서로 다른 이름으로 이해하게 되기 때문입니다.

Architecture

Turn Worker

Code

TurnReactor
TurnProcessor
FlowHandler

기능은 동작하더라도 시간이 지나면 어떤 클래스가 실제 Turn을 담당하는지 파악하기 어려워집니다.

그래서 AI에게 코드를 맡길 때도 도메인에서 사용하는 언어와 코드의 이름을 일치시키는 것이 중요합니다.

Business Language
        =
Architecture Language
        =
Code Language

AI 시대라고 해서 소프트웨어 설계 원칙이 사라지는 것이 아니라 오히려 더 중요해지는 이유입니다.


개발보다 리팩토링에 더 많은 시간을 사용할 수도 있다

AI는 기능 구현 속도가 매우 빠릅니다.

사람이 하루 동안 작성하던 코드를 훨씬 짧은 시간에 만들어낼 수도 있습니다.

하지만 코드 생산량이 증가하면 검토해야 할 코드 역시 증가합니다.

그래서 AI 개발에서는 다음과 같은 흐름이 반복됩니다.

기능 구현
 ↓
코드 검토
 ↓
구조 문제 발견
 ↓
리팩토링
 ↓
테스트
 ↓
전체 구조 재검토
 ↓
다음 기능 구현

프로젝트에서는 하나의 경험적 기준으로 개발보다 리팩토링에 더 많은 비중을 두는 접근을 강조합니다.

중요한 것은 특정 비율 자체가 아닙니다.

AI가 빠르게 코드를 생산해 준 만큼 사람이 구조를 정리하는 작업도 더 자주 해야 한다는 것입니다.


부분 수정으로 끝내지 말고 전수조사가 필요하다

AI 코드에서 하나의 설계 문제를 발견했다면 해당 파일 하나만 수정하고 끝내는 것도 위험합니다.

같은 방식으로 생성된 코드가 다른 영역에도 존재할 가능성이 높기 때문입니다.

예를 들어 Turn Worker의 책임이 잘못 분리되어 있었다면 다음처럼 접근하는 것이 좋습니다.

문제 발견
 ↓
원인 정의
 ↓
동일 패턴 검색
 ↓
전체 영향 범위 분석
 ↓
리팩토링 계획 수립
 ↓
단계별 변경
 ↓
각 단계 테스트

AI에게도 단순히 다음처럼 지시하기보다

이 파일 리팩토링해 주세요.

문제의 원인을 설명하고 전체 프로젝트에서 같은 문제가 있는지 조사하게 하는 편이 좋습니다.

현재 Turn 처리 책임이 여러 컴포넌트에 분산되어 있습니다.

Turn 도메인의 책임과 현재 아키텍처를 기준으로
프로젝트 전체에서 동일한 설계 위반을 조사하고,
영향 범위를 먼저 분석한 뒤
단계별 리팩토링 계획을 작성하세요.

각 단계가 독립적으로 테스트 가능해야 하며
기존 동작이 유지되는지 검증 방법도 함께 제시하세요.

이 차이가 단순한 코드 생성과 실제 소프트웨어 개발을 나눕니다.


AI 시대의 개발자는 아키텍트에 가까워지고 있다

AI가 코드를 잘 작성하게 되면서 프로그래밍 지식이 필요 없어졌다고 생각할 수도 있습니다.

하지만 실제로는 반대 방향으로 가고 있습니다.

코드를 입력하는 능력의 상대적인 중요성은 감소할 수 있지만 다음 능력의 중요성은 커집니다.

요구사항 분석

도메인 모델링

아키텍처 설계

역할과 책임 분리

데이터 흐름 설계

Event 설계

동시성 이해

멱등성

장애 처리

확장성

Observability

테스트 전략

코드 리뷰

리팩토링

AI는 구현 속도를 크게 높여 줍니다.

하지만 무엇을 만들 것인지, 어떤 구조로 만들어야 하는지, 현재 구현이 미래의 변경까지 견딜 수 있는지를 판단하는 것은 여전히 개발자의 영역입니다.


유지보수할 수 있어야 제품이다

프로토타입과 제품의 가장 큰 차이는 처음 만들어졌을 때 얼마나 멋지게 동작하는지가 아닙니다.

계속 변경할 수 있는가에 있습니다.

실제 서비스는 끊임없이 수정됩니다.

기능 추가
버그 수정
정책 변경
API 변경
성능 개선
보안 패치
DB 변경
UI 변경
인프라 변경

한 번의 프롬프트로 완벽한 결과물을 만드는 것보다 수십 번, 수백 번 변경해도 구조가 무너지지 않는 시스템을 만드는 것이 훨씬 어렵습니다.

그래서 AI 코딩의 목표 역시 달라져야 합니다.

코드를 빨리 만드는 것
        ↓
기능을 빨리 만드는 것
        ↓
변경 가능한 구조를 만드는 것
        ↓
지속적으로 유지보수할 수 있는 제품을 만드는 것

바이브 코딩을 잘한다는 것은 프롬프트를 잘 작성하는 것만을 의미하지 않습니다.

AI가 만들어낸 결과를 검토하고, 구조적인 문제를 발견하고, 더 나은 설계 방향을 설명하고, 프로젝트 전체를 지속적으로 리팩토링할 수 있어야 합니다.

결국 AI 시대에도 좋은 소프트웨어를 만드는 기본 원칙은 크게 달라지지 않습니다.

달라진 것은 코드를 작성하는 주체입니다.

개발자가 모든 코드를 직접 작성하던 시대에서 AI가 상당한 부분의 구현을 담당하는 시대로 이동하고 있습니다.

그만큼 개발자는 코드 작성자에서 설계자, 리뷰어, 아키텍트의 역할로 이동하고 있습니다.

AI가 구현을 더 많이 담당할수록 개발자는 더 높은 수준에서 시스템을 볼 수 있어야 합니다.

앞으로 중요한 것은 단순히 AI에게 코드를 만들어 달라고 요청하는 능력이 아닙니다.

AI가 만든 코드를 보고

왜 이렇게 설계했는가?

이 책임은 여기에 있는 것이 맞는가?

앞으로 기능이 늘어나도 이 구조를 유지할 수 있는가?

장애가 발생하면 어디에서 복구할 것인가?

같은 이벤트가 두 번 들어오면 어떻게 되는가?

Worker를 여러 대로 늘려도 문제가 없는가?

이 코드를 계속 수정하면서 운영할 수 있는가?

라고 질문할 수 있는 능력입니다.

AI가 개발자의 구현 능력을 확장해 주는 시대일수록 결국 소프트웨어 공학과 아키텍처를 이해하는 개발자의 가치가 더 커질 수밖에 없습니다.

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

0개의 댓글