
여러분은 바이브 코딩 즐겨 하시나요?
바이브코딩은 빠르고 편리하지만, 반복적으로 같은 문제에 부딪히곤 해요.
- 의도와 다른 결과물이 생성되요
- 끝없이 수정을 반복하게 되요
- 언제 내 작업이 완료되는가에 대한 기준이 사라져요
BDD 스타일의 인수테스트를 활용하면 AI에게 객관적인 검증 기준을 줄 수 있고, 이런 문제를 효과적으로 해결할 수 있어요.
이 글을 읽고 나면 아래와 같은 효과를 가질 수 있을거에요
- 바이브코딩에서 품질 문제가 반복되는 원인을 이해해요.
- BDD(행동 주도 개발) 스타일의 인수테스트가 이 문제를 어떻게 해결하는지 파악해요.
- Python-behave를 사용해 인수테스트를 직접 작성하고 실행해요.
- 인수테스트를 AI에게 검증 기준으로 넘겨서 바이브코딩 결과물의 품질을 높일 수 있어요.
바이브 코딩의 품질 문제

바이브코딩(Vibe Coding)은 AI에게 자연어로 지시해서 코드를 만드는 방식이에요.
예를 들어, "로그인 기능을 만들어 줘"라고 말하면 AI가 코드를 작성해 주죠. 빠르고 편리하지만, 막상 써보면 같은 문제에 반복적으로 부딪히게 돼요.
1. 의도와 결과의 불일치
"장바구니 기능을 만들어 줘"라고 지시했는데, AI가 만든 장바구니에는 수량 변경 기능이 없어요.
다시 "수량 변경도 추가해 줘"라고 하면, 이번에는 기존의 삭제 기능이 사라지죠.
자연어 지시는 추상적이라 AI는 매번 나름대로 해석해서 코드를 만들어요. 그런데 그 해석이 내 의도와 맞는지 확인할 방법이 없어요.
2. 검증 없이 반복 수정
"이거 고쳐 줘", "아니 그게 아니라..."를 반복하다 보면, 고칠 때마다 다른 부분이 망가져서 처음보다 상태가 나빠지기도 해요.
코드가 제대로 동작하는지 매번 직접 눈으로 확인해야 하니, 동작이 잘되는지 확인하는데에 걸리는 오버헤드 때문에 AI를 쓰는 자동화의 이점이 크게 줄어들어요.
3. "완성"의 기준 부재
언제 끝난 건지 알 수 없어요. "대충 돌아가는 것 같은데?"라는 감에 의존하게 되고, 엣지 케이스(예외 상황)도 놓치기 쉬워요.
세 가지 문제의 공통 원인은 같아요. AI에게 "무엇이 올바른 결과인지" 판단할 수 있는 객관적 기준을 주지 않았기 때문이에요.
인수테스트 라는 해결책

인수테스트란 무엇인가
인수테스트(Acceptance Test)는 "이 기능이 완성되었다고 인정하려면 어떤 조건을 충족해야 하는가"를 정의한 테스트예요.
인테리어 업체에 거실 리모델링을 맡긴다고 상상해 보세요.
- 나쁜 지시: "거실을 예쁘게 해주세요."
- 좋은 지시: "벽 색상은 화이트, 바닥은 오크 원목, 조명은 3000K 간접 조명으로 해주세요. 완료 후 조도계로 측정해서 거실 중앙 기준 300lux 이상이어야 합니다."
두 번째 지시에는 결과물을 검증할 수 있는 구체적 기준이 있어요. 인수테스트는 바로 이런 기준을 코드로 작성한 거예요.
BDD, 사람의 언어로 테스트 작성하기
BDD(Behavior-Driven Development, 행동 주도 개발)는 인수테스트를 자연어에 가까운 형식으로 작성하는 방법론이에요.
Gherkin이라는 문법을 사용하며, Given / When / Then 세 가지 키워드로 구성돼요.
- Given (주어진 상황): 테스트가 시작되기 전의 상태
- When (행동): 사용자가 수행하는 동작
- Then (결과): 기대하는 결과
장바구니 기능을 예로 들면 이런 형태예요.
Feature: 장바구니
온라인 쇼핑몰 사용자가 상품을 장바구니에 담고 관리할 수 있다.
Scenario: 장바구니에 상품 담기
Given 사용자가 상품 목록 페이지에 있다
When "Python 입문서"를 장바구니에 담는다
Then 장바구니에 "Python 입문서"가 1개 담겨 있어야 한다
Scenario: 장바구니 상품 수량 변경
Given 장바구니에 "Python 입문서"가 1개 담겨 있다
When "Python 입문서"의 수량을 3개로 변경한다
Then 장바구니에 "Python 입문서"가 3개 담겨 있어야 한다
Scenario: 장바구니 상품 삭제
Given 장바구니에 "Python 입문서"가 3개 담겨 있다
When "Python 입문서"를 장바구니에서 삭제한다
Then 장바구니가 비어 있어야 한다
비개발자라도 무엇을 검증하는지 바로 읽을 수 있다는 게 BDD의 핵심 가치예요.
인수테스트로 바이브 코딩 개선하기
인수테스트를 바이브코딩에 적용하면 워크플로우가 이렇게 바뀌어요.
[기존 바이브코딩]
추상적 지시 → AI 코드 생성 → 사람이 눈으로 확인 → 다시 지시 → 반복...
[인수테스트 기반 바이브코딩]
인수테스트 작성 → AI에게 테스트와 함께 지시
→ AI 코드 생성 → 테스트 자동 실행 → 통과/실패 즉시 확인
→ 실패 시 AI가 스스로 수정 → 전체 테스트 통과 시 완료
핵심 변화는 세 가지예요.
- "추상적 기능 구현 목표"가 "구체적 테스트 통과 목표"로 바뀌어요.
- 사람이 눈으로 하던 검증을 테스트가 자동으로 대신해요.
- AI가 스스로 결과를 판단할 수 있는 도구를 갖게 돼요.
실제로 인수테스트 적용해보기
Python 환경 설정
Python 3.8 이상이 설치되어 있어야 해요. 터미널에서 버전을 확인해 보세요.
python --version
# Python 3.11.5 (예시)
behave 설치
behave는 Python용 BDD 테스트 프레임워크로, pip로 설치할 수 있어요.
pip install behave
프로젝트 디렉토리 구조
behave는 정해진 디렉토리 구조를 따르기 때문에, 아래와 같이 만들어 주세요.
my_project/
├── features/ # behave가 인식하는 테스트 디렉토리
│ ├── cart.feature # 테스트 시나리오 파일 (Gherkin 문법)
│ └── steps/ # 테스트 시나리오를 실행하는 코드
│ └── cart_steps.py
└── cart.py # 실제 구현 코드 (AI가 작성할 대상)
저는 프로젝트 루트에 test폴더와 acceptance 폴더를 두고 그 하위에 feature 폴더를 놓아서 단위테스트와 분리해서 작성하는 식으로 작성하고 있어요!
features/와 steps/는 behave의 규약이라 이름을 바꾸면 안 돼요.
이제 간단한 장바구니 기능에 대한 인수테스트를 같이 작성해볼게요!
Feature 파일 작성
features/cart.feature 파일을 만들고 아래 내용을 입력해 주세요.
Feature: 장바구니 관리
온라인 쇼핑몰 사용자가 장바구니에 상품을 담고,
수량을 변경하고, 삭제할 수 있다.
Scenario: 장바구니에 상품 담기
Given 빈 장바구니가 있다
When "Python 입문서"를 장바구니에 담는다
Then 장바구니에 "Python 입문서"가 1개 있어야 한다
And 장바구니 총 상품 수는 1개여야 한다
Scenario: 같은 상품을 여러 번 담으면 수량이 증가한다
Given 빈 장바구니가 있다
When "Python 입문서"를 장바구니에 담는다
And "Python 입문서"를 장바구니에 담는다
Then 장바구니에 "Python 입문서"가 2개 있어야 한다
Scenario: 상품 수량 변경
Given 빈 장바구니가 있다
And "Python 입문서"를 장바구니에 담는다
When "Python 입문서"의 수량을 5개로 변경한다
Then 장바구니에 "Python 입문서"가 5개 있어야 한다
Scenario: 상품 삭제
Given 빈 장바구니가 있다
And "Python 입문서"를 장바구니에 담는다
When "Python 입문서"를 장바구니에서 삭제한다
Then 장바구니가 비어 있어야 한다
이 파일에는 구현 코드가 전혀 없어요.
"어떤 상황에서, 무엇을 하면, 어떤 결과가 나와야 하는가"만 기술하는데, 바로 이게 인수 기준(Acceptance Criteria)이에요.
Step 정의 파일 작성
Feature 파일의 각 Given/When/Then 문장을 실제로 실행하는 Python 코드를 작성할 차례예요.
features/steps/cart_steps.py 파일을 만들어 주세요.
from behave import given, when, then
from cart import Cart # 아직 존재하지 않는 파일 — 나중에 AI가 구현할 대상
@given('빈 장바구니가 있다')
def step_create_empty_cart(context):
"""테스트 시작 전, 비어 있는 장바구니를 준비한다."""
context.cart = Cart()
@when('"{item_name}"를 장바구니에 담는다')
def step_add_item(context, item_name):
"""장바구니에 상품을 하나 추가한다."""
context.cart.add(item_name)
@when('"{item_name}"의 수량을 {quantity:d}개로 변경한다')
def step_change_quantity(context, item_name, quantity):
"""장바구니에 담긴 상품의 수량을 지정된 값으로 변경한다."""
context.cart.set_quantity(item_name, quantity)
@when('"{item_name}"를 장바구니에서 삭제한다')
def step_remove_item(context, item_name):
"""장바구니에서 특정 상품을 제거한다."""
context.cart.remove(item_name)
@then('장바구니에 "{item_name}"가 {quantity:d}개 있어야 한다')
def step_check_item_quantity(context, item_name, quantity):
"""장바구니 내 특정 상품의 수량이 기대값과 일치하는지 확인한다."""
assert context.cart.get_quantity(item_name) == quantity, \
f"기대한 수량: {quantity}, 실제 수량: {context.cart.get_quantity(item_name)}"
@then('장바구니 총 상품 수는 {count:d}개여야 한다')
def step_check_total_count(context, count):
"""장바구니에 담긴 총 상품 종류 수가 기대값과 일치하는지 확인한다."""
assert context.cart.total_items() == count, \
f"기대한 총 수: {count}, 실제 총 수: {context.cart.total_items()}"
@then('장바구니가 비어 있어야 한다')
def step_check_cart_empty(context):
"""장바구니가 완전히 비어 있는지 확인한다."""
assert context.cart.is_empty(), "장바구니가 비어 있지 않습니다."
from cart import Cart에서 cart.py는 아직 존재하지 않아요.
Step 파일은 구현체가 갖춰야 할 인터페이스(add, remove, set_quantity, get_quantity, total_items, is_empty)를 미리 명세하는 역할을 해요.
테스트 먼저 실행하기
구현 코드 없이 테스트를 실행하면 당연히 실패하는데, 이게 정상적인 흐름이에요!
cd my_project
behave
Feature: 장바구니 관리
Scenario: 장바구니에 상품 담기
Given 빈 장바구니가 있다 ... ERROR (No module named 'cart')
...
0 features passed, 1 failed
모든 테스트가 실패하는 상태에서 시작하는 게 ATDD의 자연스러운 흐름이에요.
이제 이 실패하는 테스트를 AI에게 넘겨서 통과시키면 돼요.
인수테스트 기반 바이브코딩

인수테스트 없이 바이브 코딩
AI에게 이렇게 지시한다고 해볼게요.
장바구니 기능을 구현해 줘. 상품 추가, 수량 변경, 삭제가 되어야 해.
AI는 나름대로 코드를 생성하지만, 결과를 확인하려면 직접 코드를 읽거나 실행해 봐야 해요.
같은 상품을 두 번 담았을 때 수량이 증가하는지, set_quantity에 0을 넘기면 어떻게 되는지 같은 엣지 케이스는 지시에 없었으니 AI가 처리하지 않았을 가능성이 높아요.
결국 "수량이 안 올라가는데 고쳐 줘"라는 추가 지시가 이어지고, 수정할 때마다 다른 부분이 깨지는 악순환이 시작돼요.
인수테스트와 함께 바이브코딩
이번에는 앞에서 작성한 Feature 파일과 Step 파일을 AI에게 함께 프롬프트로서 사용해볼게요.
아래 인수테스트를 모두 통과하는 cart.py를 구현해 줘.
테스트는 behave로 실행하며, 모든 시나리오가 통과해야 해.
[features/cart.feature 내용 첨부]
[features/steps/cart_steps.py 내용 첨부]
AI는 이제 명확한 목표가 생겨요.
Step 파일에서 Cart 클래스에 add, remove, set_quantity, get_quantity, total_items, is_empty 메서드가 필요하다는 걸 읽을 수 있어요.
Feature 파일에서는 각 메서드가 어떤 상황에서 어떤 결과를 반환해야 하는지도 파악할 수 있고요.
AI가 생성한 코드는 이런 형태예요.
# cart.py — AI가 인수테스트를 기반으로 생성한 코드
class Cart:
def __init__(self):
self._items = {} # {상품명: 수량}
def add(self, item_name: str) -> None:
"""상품을 장바구니에 추가한다. 이미 있으면 수량을 1 증가시킨다."""
if item_name in self._items:
self._items[item_name] += 1
else:
self._items[item_name] = 1
def remove(self, item_name: str) -> None:
"""상품을 장바구니에서 제거한다."""
if item_name in self._items:
del self._items[item_name]
def set_quantity(self, item_name: str, quantity: int) -> None:
"""상품의 수량을 지정된 값으로 변경한다."""
if item_name in self._items:
self._items[item_name] = quantity
def get_quantity(self, item_name: str) -> int:
"""상품의 현재 수량을 반환한다."""
return self._items.get(item_name, 0)
def total_items(self) -> int:
"""장바구니에 담긴 상품 종류 수를 반환한다."""
return len(self._items)
def is_empty(self) -> bool:
"""장바구니가 비어 있는지 반환한다."""
return len(self._items) == 0
테스트 실행으로 검증하기
behave
Feature: 장바구니 관리
Scenario: 장바구니에 상품 담기 ... passed
Scenario: 같은 상품을 여러 번 담으면 수량이 증가한다 ... passed
Scenario: 상품 수량 변경 ... passed
Scenario: 상품 삭제 ... passed
1 feature passed, 0 failed
4 scenarios passed, 0 failed
12 steps passed, 0 failed
4개의 시나리오, 12개의 스텝이 모두 통과했어요. 이 시점에서 "장바구니 기능이 완성되었다"라고 객관적으로 판단할 수 있어요.
두 방식의 차이를 정리하면 이래요.
| 비교 항목 | 테스트 없이 바이브코딩 | 인수테스트 기반 바이브코딩 |
| AI에게 주는 정보 | 추상적 자연어 설명 | 구체적 시나리오 + 검증 코드 |
| 완료 기준 | 사람의 감각적 판단 | 테스트 전체 통과 |
| 검증 방법 | 직접 실행·확인 | 자동화된 테스트 실행 |
| 수정 시 안전성 | 다른 기능이 깨질 수 있음 | 회귀 테스트로 기존 기능 보호 |
| 엣지 케이스 처리 | 누락되기 쉬움 | 시나리오로 사전에 정의 |
좋은 인수테스트를 작성하는 방법

시나리오는 사용자 관점에서 작성한다
내부 구현이 아니라 사용자가 경험하는 행동을 기술해 주세요. 코드라기보단 문서를 작성한다고 생각하면 좋아요.
# 나쁜 예: 내부 구현을 노출
Scenario: 딕셔너리에 아이템 키 추가
Given _items 딕셔너리가 비어 있다
When _items["책"] = 1을 실행한다
Then _items의 길이가 1이어야 한다
# 좋은 예: 사용자 행동에 집중
Scenario: 장바구니에 상품 담기
Given 빈 장바구니가 있다
When "책"을 장바구니에 담는다
Then 장바구니에 "책"이 1개 있어야 한다
시나리오 하나에 검증 하나
하나의 시나리오에 너무 많은 것을 검증하려 하지 마세요.
시나리오 하나가 하나의 행동과 결과만 검증해야 실패했을 때 원인을 빠르게 파악할 수 있어요.
Scenario Outline으로 반복 줄이기
비슷한 시나리오가 여러 개라면 Scenario Outline으로 반복을 줄일 수 있어요.
Scenario Outline: 다양한 상품을 장바구니에 담기
Given 빈 장바구니가 있다
When "<상품명>"를 장바구니에 담는다
Then 장바구니에 "<상품명>"가 1개 있어야 한다
Examples:
| 상품명 |
| Python 입문서 |
| Java 입문서 |
| Go 입문서 |
공통 Step 선언으로 반복 줄이기
프로젝트가 커지면 여러 Feature 파일에서 같은 동작을 반복하게 돼요.
"POST 요청을 보낸다", "응답 상태 코드를 확인한다" 같은 Step은 거의 매번 등장하죠.
이런 Step을 매번 새로 작성하면 중복이 쌓이고, 수정할 때도 여러 파일을 고쳐야 해요.
이럴 때 공통 Step 파일을 하나 만들어 두면 훨씬 편해요. 예를 들어 API 테스트에서 자주 쓰는 요청·응답 검증 Step을 features/steps/common_steps.py에 모아둘 수 있어요.
# features/steps/common_steps.py
from behave import given, when, then
@given("API 서버가 실행 중이다")
def step_api_server_running(context):
assert context.client is not None
@when('"{path}" 경로로 GET 요청을 보낸다')
def step_send_get_request(context, path):
context.response = context.client.get(path)
@when('"{path}" 경로로 다음 데이터와 함께 POST 요청을 보낸다')
def step_send_post_request_with_data(context, path):
import json
data = json.loads(context.text)
context.response = context.client.post(path, json=data)
@then("응답 상태 코드는 {status_code:d}이다")
def step_check_status_code(context, status_code):
assert context.response.status_code == status_code
@then('응답에 "{field}" 필드가 존재한다')
def step_check_response_has_field(context, field):
assert field in context.response.json()
@then('응답의 "{field}" 값은 "{value}"이다')
def step_check_response_field_value(context, field, value):
assert context.response.json()[field] == value
이렇게 만들어 두면 Feature 파일에서 바로 가져다 쓸 수 있어요.
Feature: 사용자 API
Scenario: 사용자 생성
Given API 서버가 실행 중이다
When "/users" 경로로 다음 데이터와 함께 POST 요청을 보낸다
"""
{"name": "홍길동", "email": "hong@example.com"}
"""
Then 응답 상태 코드는 201이다
And 응답의 "name" 값은 "홍길동"이다
공통 Step이 잘 갖춰져 있으면 새로운 Feature를 추가할 때 Step 정의 없이 시나리오만 작성하면 돼요.
AI에게 넘길 때도 공통 Step 파일을 함께 첨부하면, AI가 이미 정의된 Step을 파악하고 일관된 인터페이스로 코드를 구현할 수 있어요.
바이브코딩의 핵심 문제는 AI가 잘못해서가 아니라, 우리가 "무엇이 올바른 결과인지"를 알려주지 않았기 때문에 생겨요.
인수테스트는 그 기준을 코드로 만들어 AI에게 건네는 가장 실용적인 방법입니다.
여러분 프로젝트에도 한번 적용해보시고 폭발적인 바이브 코딩 능률을 느껴보세요!
안녕하세요, 저는 주니어 개발자 박석희 입니다. 언제든 하단 연락처로 연락주세요 😆