[Playback Gate #4] 회고

2026. 8. 14. 21:31ㆍPROJECT/Toy Project

[Playback Gate #4] 바이브 코딩 전략을 실제로 적용해본 결과

1. 시작하며

이전 글에서는 Playback Gate를 개발하면서 AI Coding Assistant를 어떤 방식으로 활용할지 먼저 정리했다.

바이브 코딩 전략

당시 가장 중요하게 생각했던 원칙은 다음과 같았다.

AI는 개발을 대신 판단하는 존재가 아니라, 정해진 요구사항과 설계를 빠르게 코드로 옮기고 검토와 개선을 보조하는 도구로 사용한다.

단순히

"Playback Gate 만들어줘"
        ↓
AI가 전체 코드 생성
        ↓
완성

하는 방식으로 개발하고 싶지는 않았다.

대신 요구사항과 설계를 문서로 남기고, 작업을 작은 단위로 나눈 뒤 테스트와 측정 결과를 기준으로 다음 작업을 결정하는 방식으로 진행했다.

프로젝트가 끝난 지금 돌아보면 처음 작성했던 전략과 실제 구현이 완전히 같지는 않았다.

특히 처음에는 Redis, Index, Redis Lock 같은 기술을 자연스럽게 사용하게 될 것이라고 생각했지만 실제 측정 결과는 다른 방향을 가리켰다.

이번 글에서는 처음 정했던 바이브 코딩 전략을 실제 프로젝트에 어떻게 적용했는지, 그리고 개발이 끝난 뒤 어떤 부분이 달라졌는지를 정리해보려고 한다.

GitHub 저장소는 다음과 같다.


2. 처음 정했던 바이브 코딩 방식

처음에는 AI에게 프로젝트 전체를 한 번에 구현시키지 않기로 했다.

대신 프로젝트 내부의 문서를 AI가 참고하는 Context로 사용했다.

REQUIREMENTS.md
→ 무엇을 만들어야 하는가

DESIGN.md
→ 현재 시스템은 어떻게 되어 있는가

ROADMAP.md
→ 어떤 순서로 진행할 것인가

TASKS.md
→ 지금 무엇을 해야 하는가

adr/
→ 왜 이런 기술적 결정을 했는가

experiments/
→ 실제 측정 결과는 어땠는가

그리고 전체 개발 흐름은 다음과 같이 잡았다.

요구사항
   ↓
설계
   ↓
Task 분해
   ↓
AI 구현
   ↓
Test
   ↓
측정
   ↓
문제 분석
   ↓
개선
   ↓
재측정

중요한 점은 AI가 먼저 정답을 정하지 않게 하는 것이었다.

예를 들어 AI가

Redis Cache를 적용하는 것이 좋습니다.

라고 제안하더라도 바로 적용하지 않는다.

먼저 실제 부하 테스트에서 DB 조회가 병목인지 확인하고, Cache가 필요하다면 Local Cache와 Redis를 비교한 뒤 선택하는 방식으로 진행했다.

동시성 문제 역시 마찬가지였다.

DB Pessimistic Lock
Redis Distributed Lock
JVM Lock

중 하나를 처음부터 정하지 않고 Race Condition을 실제로 재현한 뒤 선택하기로 했다.


3. 실제 프로젝트에서는 어떻게 적용했는가

3.1 문서와 Cursor Skill을 Harness로 사용했다

전략 글에서 정리했던 Harness 개념을 실제 저장소 안에 넣었다.

실제 프로젝트에서는 다음과 같은 구조를 사용했다.

docs/
├── design.md
├── requirements.md
├── roadmap.md
├── TASKS.md
│
├── adr/
│   ├── 002-content-cache-strategy.md
│   └── 003-concurrency-control.md
│
└── experiments/
    ├── 001-baseline.md
    ├── 002-db-analysis.md
    ├── 003-cache.md
    ├── 004-concurrency.md
    ├── 005-load-test.md
    ├── 006-spike-test.md
    └── 007-final-comparison.md

.cursor/
└── skills/

문서 이름은 처음 전략과 반대로 붙은 부분이 있다.

docs/design.md
→ 요구사항

docs/requirements.md
→ 현재 설계

처음 계획에서는 REQUIREMENTS.md = 요구사항, DESIGN.md = 현재 설계로 두려고 했기 때문에 이 부분은 다음 프로젝트에서는 맞추는 것이 좋을 것 같다.

하지만 문서의 역할 자체는 구분해서 사용했다.

AI와 대화가 길어지더라도

Redis를 성능상 필요할 것 같다는 이유만으로 먼저 추가하지 않는다.

같은 프로젝트 원칙이 문서와 Skill에 남아 있기 때문에 매번 Prompt로 다시 설명할 필요가 없었다.

이 부분은 실제로 써보니 꽤 유용했다.


3.2 Phase 단위로 Loop를 돌렸다

한 번에 전체 프로젝트를 구현하지 않고 ROADMAP 기준으로 Phase를 나눴다.

Phase 1
Baseline 애플리케이션 구현
        ↓
Phase 2
Baseline 성능 측정
        ↓
Phase 3
DB 병목 분석
        ↓
Phase 4
Cache 검증
        ↓
Phase 5
동시성 문제 재현 및 해결
        ↓
Phase 6
Load / Stress / Spike Test
        ↓
Phase 7
최종 비교

각 Phase가 끝날 때마다 TASKS.md의 현재 작업을 변경했다.

예를 들어 Phase 2에서는 성능 문제를 발견하더라도 바로 수정하지 않았다.

Baseline 측정
      ↓
결과 기록
      ↓
Phase 종료

까지만 진행했다.

그 다음 Phase에서 이전 측정값을 기준으로 병목을 분석했다.

AI에게도 단순히

성능 개선해줘.

라고 요청하지 않고,

현재 RPS
p50
p95
Hikari pending
DB Query 수

같은 직전 실험 결과를 제공한 뒤 다음에 바꿀 한 가지를 판단하도록 했다.

처음 전략 글에서 생각했던 Human-controlled Loop에 가장 가까운 부분이었다.


3.3 불필요한 기능과 추상화를 제한했다

Playback Gate의 목적은 실제 OTT 전체 서비스를 구현하는 것이 아니었다.

그래서 다음 기능들은 끝까지 넣지 않았다.

  • 회원가입 UI
  • 관리자 페이지
  • 실제 영상 스트리밍
  • DRM
  • CDN
  • Kafka
  • Microservice

코드 구조도 마찬가지였다.

사용처가 하나뿐인 Interface나 Factory를 미리 만들지 않고, PlaybackService가 재생 권한 검증 순서를 비교적 그대로 따라가도록 만들었다.

AI가 미래 확장을 이유로 구조를 크게 만드는 것보다 현재 문제를 이해하기 쉬운 코드로 유지하는 것을 우선했다.


3.4 AI가 완료했다고 말하는 것을 완료 조건으로 사용하지 않았다

각 Phase의 완료 조건은 테스트와 실험 결과였다.

기본 기능은 다음 테스트를 사용했다.

  • PlaybackServiceTest
  • PlaybackIntegrationTest
  • ConcurrentPlaybackTest

특히 동시성 문제에서는 Lock을 먼저 넣지 않았다.

BASIC 이용권의 동시 재생 가능 수가 1인 상태에서 40개의 요청을 동시에 발생시켰다.

Lock이 없는 상태에서는 결과가 다음과 같았다.

동시 요청 = 40
HTTP 200 = 40
ACTIVE Session = 40
허용되어야 하는 Session = 1

즉 Race Condition이 실제로 재현된 것을 확인한 뒤에야 해결 방법을 적용했다.

이 과정은 처음 세운

문제를 예상해서 미리 막지 말고 실제로 재현한 뒤 해결한다.

라는 원칙을 그대로 적용한 부분이다.


4. 처음 예상과 가장 달랐던 기술 선택

프로젝트를 시작할 때는 Redis나 Index를 자연스럽게 사용하게 될 것이라고 생각했다.

하지만 실제 결과는 꽤 달랐다.


4.1 DB 병목이라고 해서 Index를 추가하지 않았다

Baseline 테스트에서는 Hikari Connection Pool이 10개로 가득 차고 대기 요청이 발생했다.

처음에는 Index 문제도 의심했다.

하지만 Query 실행 계획을 확인했을 때 핵심 COUNT Query가 Full Scan으로 인해 무너지는 상황은 아니었다.

그래서 Phase 3에서는 Index를 억지로 추가하지 않고 Connection Pool을 먼저 조정했다.

Hikari Maximum Pool Size

10
↓
40

결과는 다음과 같았다.

Iteration RPS
321 → 326 수준

Pool 대기는 줄었지만 처리량 자체는 거의 증가하지 않았다.

즉,

Connection Pool이 가득 차 있다.
→ Pool을 늘리면 서버가 빨라질 것이다.

라는 가설은 절반만 맞았다.

대기열은 줄었지만 실제 처리량은 거의 그대로였고 p50은 오히려 나빠졌다.

재측정하지 않았다면 단순히 Connection Pool을 늘려서 성능을 개선했다고 잘못 결론 내릴 수도 있었던 부분이다.


4.2 Cache는 Redis가 아니라 Caffeine이었다

다음으로 반복되는 조회를 확인했다.

Playback 요청마다 다음 데이터가 계속 조회되고 있었다.

Member
Content
Subscription

처음 계획에서는 자연스럽게 Redis를 후보로 생각했다.

하지만 당시 애플리케이션 인스턴스는 하나였다.

여러 서버가 Cache를 공유할 필요가 없는데 Redis를 추가하면 네트워크와 인프라만 늘어난다.

그래서 다음 후보를 비교했다.

DB Only
Caffeine Local Cache
Redis

최종적으로 선택한 것은 Caffeine Local Cache였다.

실제 측정 결과도 의미가 있었다.

지표 Cache 적용 전 Caffeine 적용 후
p50 21.63 ms 15.82 ms
p95 33.88 ms 26.82 ms
p99 44.07 ms 36.10 ms
RPS 326.0 321.5

Cache Hit Rate도 다음과 같이 나왔다.

Content      약 99.9%
Member       약 90.5%
Subscription 약 90.5%

Latency는 줄었지만 RPS는 거의 그대로였다.

또한 PlaybackSession COUNT는 Cache 대상에서 제외했다.

동시 재생 수는 즉시 정확해야 하기 때문에 이 값을 Cache하면 오히려 핵심 요구사항을 깨뜨릴 수 있기 때문이다.

결국 Cache를 사용한다는 것보다 어떤 데이터를 Cache하면 안 되는지 판단하는 과정이 더 중요했다.


4.3 동시성 제어도 Redis Lock이 아니었다

동시성 문제를 재현한 뒤 다음 방식을 비교했다.

DB Pessimistic Lock
JVM Lock
Redis Lock / Atomic

JVM Lock은 현재 단일 애플리케이션에서는 동작하지만 서버를 여러 대로 늘리면 같은 문제가 다시 발생한다.

Redis Lock은 여러 서버에서도 사용할 수 있지만 현재 구조에서는 Redis 자체가 필요하지 않았다.

결국 선택한 방식은 Subscription 행에 대한 DB FOR UPDATE였다.

Subscription 조회
        ↓
SELECT ... FOR UPDATE
        ↓
ACTIVE Session COUNT
        ↓
Session INSERT

다만 여기서 예상하지 못했던 문제가 하나 있었다.

MySQL 기본 Isolation Level인 REPEATABLE READ 상태에서는 앞선 SELECT가 Snapshot을 만들면서 Lock 대기 중 다른 Transaction이 생성한 Session을 COUNT에서 보지 못하는 상황이 발생했다.

그래서 Playback 시작 Transaction에는 READ_COMMITTED를 함께 적용했다.

PESSIMISTIC_WRITE
+
READ_COMMITTED

적용 후 같은 40개 동시 요청 테스트에서는 다음과 같이 바뀌었다.

Lock 적용 전
ACTIVE Session = 40

Lock 적용 후
ACTIVE Session = 1

대신 비용도 있었다.

지표 Lock 적용 전 Lock 적용 후
Iteration RPS 321.5 296.0
p50 15.82 ms 21.41 ms
p95 26.82 ms 32.61 ms
p99 36.10 ms 41.31 ms

Lock은 성능을 높이기 위해 넣은 것이 아니라 정확성을 보장하기 위해 넣은 것이다.

이 차이를 실제 수치로 확인할 수 있었다.


5. 최종 결과는 Baseline보다 무조건 빨라지지 않았다

처음 프로젝트를 시작할 때는 여러 단계를 거치면 당연히 최종 RPS도 올라갈 것이라고 생각했다.

하지만 동일한 k6 Scenario로 Baseline과 Final을 비교한 결과는 다음과 같았다.

항목 Baseline Final
Iteration RPS 321.5 296.0
p50 14.61 ms 21.41 ms
p95 34.62 ms 32.61 ms
BASIC 40 동시 요청 ACTIVE 이후 테스트에서 40으로 재현 1

처리량만 보면 오히려 낮아졌다.

그렇다고 Final이 실패한 구조라고 보기는 어렵다.

프로젝트의 핵심 요구사항 중 하나는 동시 재생 제한을 정확하게 지키는 것이었기 때문이다.

Baseline에서는 빠르게 처리하는 대신 같은 계정에서 동시에 요청하면 재생 한도가 깨졌다.

Final에서는 Lock 비용이 추가되면서 처리량은 감소했지만 다음 요구사항을 만족하게 됐다.

BASIC 최대 동시 재생 = 1

40개 동시 요청
        ↓
ACTIVE Session = 1

이번 프로젝트에서 가장 크게 느낀 부분이기도 하다.

성능 최적화는 무조건 RPS를 높이는 작업이 아니다.

정확성, Latency, 처리량, 인프라 복잡도 사이에서 어떤 것을 선택해야 하는지도 함께 봐야 한다.


6. 처음 작성했던 전략과 달라진 부분

6.1 실험 파일 이름에서 기술 이름을 빼게 됐다

처음 전략을 작성할 때는 실험 파일 예시를 다음과 같이 생각했다.

002-db-index.md
003-redis-cache.md

하지만 실제 프로젝트에서는 다음처럼 바뀌었다.

002-db-analysis.md
003-cache.md
004-concurrency.md

이 방식이 오히려 처음 세웠던 원칙에 더 잘 맞았다.

실험을 시작하기 전부터 파일 이름에 Index, Redis를 넣어버리면 이미 해결 방법을 정한 것이기 때문이다.

앞으로도 실험 문서는 기술 이름보다 확인하려는 문제를 중심으로 이름을 짓는 편이 좋을 것 같다.


6.2 Cursor Skill은 생각보다 단순하게 유지했다

처음에는 구현, 테스트, 실험, 설계 변경마다 별도의 Skill을 만들 수도 있다고 생각했다.

실제로는 playback-gate-vibe-coding Skill 하나와 docs/ 문서만으로 충분했다.

프로젝트 규모가 크지 않은 상태에서 Skill을 지나치게 나누는 것보다 문서의 역할을 분명하게 유지하는 것이 더 중요했다.


6.3 Agent Graph는 사용하지 않았다

전략 글에서는 다음과 같은 Graph도 예시로 생각했다.

Coding Agent
    ↓
Test Agent
    ↓
Review Agent
    ↓
Gate

하지만 이번 프로젝트에서는 사용하지 않았다.

실제 개발 과정은 다음에 더 가까웠다.

AI Plan
  ↓
사람 Review
  ↓
AI Implement
  ↓
Test / k6
  ↓
사람 판단
  ↓
다음 Task

즉 Harness + Human-controlled Loop 정도에서 끝냈다.

Playback Gate 정도의 규모에서는 여러 Agent를 연결하는 것이 얻는 이점보다 구조적인 복잡성이 더 크다고 판단했다.

처음부터 Graph Engineering까지 적용하지 않기로 했던 판단은 결과적으로 괜찮았다고 생각한다.


7. GitHub에는 어떻게 정리했는가

개발 과정에서 매 Phase를 GitHub에 계속 Push한 것은 아니다.

Phase 1부터 Phase 7, README 정리까지 로컬에서 모두 진행한 뒤 마지막에 빈 Repository를 만들고 한 번에 올렸다.

git init -b main
git add
git commit
git remote add origin
git push -u origin main

원격 저장소는 SSH를 사용했다.

git@github.com:00700lsm/tving-playback-gate.git

프로젝트 실행에 필요하지 않거나 로컬 환경에만 필요한 파일은 제외했다.

.jdk/
build/
.gradle/
load-test/k6/results/
.env
.blog/

GitHub 첫 화면의 README.md는 아키텍처 설명만 적기보다 프로젝트를 실제로 어떻게 확인할 수 있는지를 중심으로 작성했다.

예를 들어 다음 내용을 바로 확인할 수 있게 했다.

  • JUnit 실행 방법
  • JWT를 이용한 API 호출 방법
  • Grafana 확인 방법
  • k6 부하 테스트 실행 방법

Playback Gate에는 로그인 UI가 없기 때문에 브라우저에서 API 주소만 열면 UNAUTHORIZED가 발생한다.

그래서 저장소를 처음 보는 사람도

이 프로젝트를 어떻게 실행하고 어떤 테스트를 돌려야 하는가?

를 바로 알 수 있도록 README를 구성했다.

이 부분은 처음 바이브 코딩 전략을 작성할 때는 생각하지 않았지만, 결과적으로는 사람이 저장소를 이해하기 위한 Harness 역할도 하고 있다고 생각한다.


8. 실제 적용한 것과 적용하지 않은 것

방식 처음 계획 실제 프로젝트
Vibe Coding 자연어로 AI와 개발 사용했지만 Phase / Task 단위로 제한
Harness 문서·규칙·테스트 docs + Cursor Skill + README
Loop Plan → Implement → Test Phase 1~7 전체에서 사용
Graph 여러 Agent 연결 사용하지 않음
Redis Cache / Lock 후보 필요성이 없어 사용하지 않음
Cache Redis 가능성 검토 Caffeine 선택
동시성 DB / JVM / Redis 비교 DB FOR UPDATE 선택
DB 개선 Index 등 검토 Hikari 조정, Index는 적용하지 않음

결과적으로 처음 생각했던 기술들을 모두 사용하는 프로젝트는 되지 않았다.

오히려 사용하지 않을 이유를 측정으로 설명할 수 있게 된 것이 이번 프로젝트에서 더 의미 있었던 부분이라고 생각한다.


9. 다음에 같은 방식으로 개발한다면

이번 프로젝트를 다시 진행한다면 몇 가지는 처음부터 다르게 할 것 같다.

첫 번째는 문서 이름을 처음 정한 규칙과 맞추는 것이다.

REQUIREMENTS.md
→ 요구사항

DESIGN.md
→ 현재 설계

두 번째는 실험 파일에 정답처럼 보이는 기술 이름을 미리 넣지 않는 것이다.

redis-cache
index-test

보다는

cache-analysis
db-analysis

처럼 문제 중심으로 이름을 짓는 편이 낫다.

세 번째는 Git History다.

이번에는 로컬에서 프로젝트 전체를 완료한 뒤 한 번에 GitHub에 올렸다.

다음에는 최소한 다음 정도는 Commit을 분리해보고 싶다.

Race Condition 재현
        ↓
동시성 해결

이렇게 하면 코드만 봐도 문제가 있었던 상태와 해결한 상태를 비교하기 쉬워지기 때문이다.

마지막으로 성능 개선을 평가할 때는 앞으로도 반드시 같은 조건의 측정값을 남길 생각이다.

변경
 ↓
좋아 보임

이 아니라

변경 전 측정
 ↓
변경
 ↓
동일 조건 재측정
 ↓
결과 비교

방식을 유지하려고 한다.


10. 정리

Playback Gate에서 바이브 코딩을 사용하면서 가장 중요했던 것은 AI에게 코드를 얼마나 많이 작성시키느냐가 아니었다.

오히려 다음 세 가지를 계속 개발자가 통제하는 것이 중요했다.

무엇을 만들 것인가
→ 요구사항

어떤 구조를 선택할 것인가
→ DESIGN + ADR

실제로 좋아졌는가
→ TEST + experiments

처음에는 Redis, Index, Redis Lock 같은 기술이 자연스럽게 최종 구조에 들어갈 것이라고 생각했다.

하지만 실제 측정 결과 최종 구조는 다음과 같이 남았다.

Spring Boot
   │
   ├─ Caffeine
   │
   └─ MySQL
        │
        └─ Subscription FOR UPDATE

Redis는 끝까지 사용하지 않았다.

Hikari Pool을 늘려도 처리량은 거의 증가하지 않았고, Cache는 Redis보다 Caffeine이 현재 구조에 더 적합했다.

동시성 Lock을 적용한 뒤에는 RPS가 오히려 감소했지만 동시 재생 한도는 정확하게 지켜졌다.

결국 이번 프로젝트에서 가장 크게 얻은 것은 특정 기술 사용 경험보다는 다음 흐름을 직접 반복해본 경험이었다.

문제 재현
   ↓
측정
   ↓
원인 분석
   ↓
대안 비교
   ↓
선택
   ↓
적용
   ↓
재측정

바이브 코딩도 마찬가지라고 생각한다.

AI에게 판단을 전부 맡기는 것이 아니라 AI가 빠르게 구현할 수 있는 환경을 만들고, 중요한 판단과 검증은 개발자가 계속 가지고 있는 방식이 현재로서는 가장 잘 맞았다.

반응형