
요즘 개발자들 사이에서 AI 바이브 코딩이라는 말, 참 자주 들리죠. 저도 20년 넘게 개발자로 밥벌이를 하다 보니 처음엔 솔직히 좀 시큰둥했어요. “아, 또 새로운 이름 붙은 유행 하나 지나가겠구나” 싶었거든요. 그런데 얼마 전에 FastAPI로 작은 API 서버를 만들 일이 있었고, 그때 AI 코딩 도구를 꽤 진지하게 써봤습니다. 써보고 나니 생각이 조금 바뀌었어요. 이게 단순히 AI한테 코드를 맡기는 일이 아니더라고요. 내가 알고 있는 구조와 기준 위에 AI가 초안을 던져주고, 그걸 내가 보고 고치고 버릴 건 버리는 작업에 가깝습니다. 뭐랄까, 일 잘하는데 아직 세상 물정은 모르는 주니어랑 같이 일하는 느낌이랄까요.
그래서 오늘은 제가 실제로 FastAPI 서버를 만들면서 AI 바이브 코딩을 어떻게 활용했는지, 어디서 편했고 어디서 뒤통수를 맞았는지, 그리고 다음에 다시 한다면 어떤 식으로 쓸 건지 편하게 얘기해보려고 합니다. 너무 교과서처럼 정리된 글 말고요. 그냥 40대 개발자 친구가 커피 한 잔 앞에 두고 “야, 이거 써보니까 말이야…” 하고 떠드는 느낌으로 봐주시면 좋겠습니다.
AI 바이브 코딩, 결국 핵심은 AI에게 얼마나 잘 맡기느냐였어요
사실 AI 바이브 코딩이라는 말만 들으면 뭔가 대단해 보이잖아요. 막 감으로 코딩하고, AI가 알아서 전체 시스템을 완성해주고, 나는 옆에서 고개만 끄덕이면 되는 그런 그림이 떠오르기도 하고요. 그런데 실제로 해보면 그 정도는 아닙니다. 아직은 아니에요. 적어도 제가 FastAPI 프로젝트에서 써본 느낌으로는, AI는 전체 설계를 책임지는 동료라기보다는 초안을 엄청 빠르게 만들어주는 조수에 더 가까웠습니다.
예전에는 모르는 게 있으면 Stack Overflow 뒤지고, 공식 문서 보고, GitHub 예제 코드를 찾아봤잖아요. 지금은 그 첫 단계를 AI가 확 줄여줍니다. “FastAPI로 여행지 정보를 저장하는 API 만들어줘”라고 말하면 뭔가 그럴듯한 코드가 바로 나와요. 처음 보면 꽤 감동적입니다. 저도 처음엔 “오, 이 정도면 주말에 사이드 프로젝트 하나 만들겠는데?” 싶었어요.
제가 만들었던 건 여행 블로그를 운영하는 지인을 위한 작은 API였습니다. 다녀온 여행지 이름, 설명, 위치, 태그 같은 걸 저장하고 조회하는 서비스였어요. 아주 거창한 건 아니지만, 실사용을 염두에 두면 데이터 검증이나 에러 처리, DB 연결 같은 기본기가 은근히 중요하죠.
처음에 AI에게 이렇게 물어봤습니다.
FastAPI로 여행지 데이터를 저장하는 CRUD API 예제를 만들어줘.
그러자 AI가 금방 이런 코드를 내놓더라고요.
from fastapi import FastAPI
app = FastAPI()
@app.post("/travels")
async def create_travel(name: str, description: str):
return {"message": "success"}
처음 보면 나쁘지 않아 보입니다. 돌아가기도 할 거예요. 그런데 운영하는 API 관점에서 보면 허술한 부분이 바로 보입니다. 데이터 검증이 거의 없어요. FastAPI의 장점 중 하나가 Pydantic 기반의 요청 검증인데, 그걸 제대로 살리지 못한 코드였죠.
예를 들어 여행지 이름이 빈 문자열이어도 들어올 수 있고, description이 너무 길어도 막을 방법이 없습니다. 이 정도는 개인 테스트에서는 그냥 넘어갈 수 있지만, 실제 서비스에서는 나중에 꼭 문제가 됩니다. 데이터가 망가지는 건 조용히 오거든요. 장애처럼 시끄럽게 터지면 차라리 빨리 발견하는데, 데이터 품질 문제는 한참 지나고 나서 “어? 이게 왜 이래?” 하면서 사람을 피곤하게 만듭니다.
| 구분 | AI가 처음 제안한 방식 | 제가 고친 방식 |
|---|---|---|
| 요청 데이터 처리 | 함수 파라미터로 name, description을 바로 받음 | Pydantic 모델로 요청 스키마를 분리 |
| 검증 | 거의 없음 | 필수값, 최소 길이, 최대 길이를 명시 |
| 유지보수 | 엔드포인트가 늘어나면 중복이 생기기 쉬움 | 스키마를 재사용하기 쉬움 |
그래서 저는 바로 이런 식으로 바꿨습니다.
from pydantic import BaseModel, Field
from typing import Optional
class TravelCreateRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=1000)
location: Optional[str] = Field(None, max_length=200)
그리고 엔드포인트는 이렇게 받도록 정리했죠.
@app.post("/travels")
async def create_travel(request: TravelCreateRequest):
return {
"message": "success",
"data": request
}
이건 정말 작은 차이처럼 보이지만, 나중에 API가 커질수록 차이가 큽니다. 제 경험상 이런 기본 검증만 잘 넣어도 이상한 요청 때문에 생기는 자잘한 에러가 꽤 줄어듭니다. 특히 프론트엔드와 협업할 때는 더 그렇고요. “이 값 왜 비어 있어요?” 같은 대화가 줄어드는 것만으로도 개발자는 평화를 얻습니다.
제가 여기서 얻은 첫 번째 감은 이거였어요. AI한테 그냥 “API 만들어줘”라고 하면 너무 느슨한 코드가 나옵니다. 대신 “Pydantic 모델을 먼저 정의하고, 요청값 검증을 포함해서 만들어줘”라고 말하면 결과가 훨씬 좋아져요. AI도 결국 내가 준 지시의 해상도만큼 움직이는 것 같습니다.
FastAPI에서 AI가 자주 삐끗했던 부분은 비동기 처리였어요
FastAPI 하면 역시 async 이야기를 빼놓을 수 없죠. 저도 FastAPI를 좋아하는 이유 중 하나가 비동기 처리를 비교적 깔끔하게 가져갈 수 있다는 점인데요. 문제는 AI가 이 async를 너무 자신 있게, 때로는 너무 아무 데나 붙인다는 겁니다.
제가 실제로 겪은 에러 중 하나가 DB 세션 처리였습니다. AI에게 SQLAlchemy를 붙여달라고 했더니, 겉보기에는 멀쩡한 async 코드를 만들어줬어요. 그런데 실행해보니 계속 세션 관련 오류가 나더라고요. 비슷한 메시지가 이런 식이었습니다.
RuntimeError: You cannot use AsyncSession with sync engine or sync session configuration
처음엔 제가 설정을 잘못했나 싶었습니다. 그런데 코드를 자세히 보니 AI가 AsyncSession을 쓰는 척하면서 실제 engine 설정은 동기 방식에 가깝게 섞어놓은 상태였어요. 그러니까 겉에는 async 옷을 입혀놨는데, 속은 sync 방식인 거죠. 이런 코드가 제일 위험합니다. 얼핏 보면 그럴듯하거든요.
그때부터 저는 질문 방식을 바꿨습니다. 한 번에 “FastAPI랑 SQLAlchemy로 전체 CRUD 만들어줘”라고 하지 않았어요. 대신 아주 잘게 쪼개서 물어봤습니다.
- FastAPI에서 SQLAlchemy 2.0을 async 방식으로 쓸 때 필요한 패키지를 알려줘.
- async engine을 만드는 database.py 예제를 보여줘.
- AsyncSession을 Depends로 주입하는 get_db 함수를 만들어줘.
- 이 구조를 기준으로 Travel 모델을 만들어줘.
- 이제 Travel 모델을 저장하는 service 함수를 만들어줘.
이렇게 나눠서 시키니까 확실히 실수가 줄었습니다. AI도 한 번에 넓은 범위를 처리할 때보다, 경계가 분명한 작은 작업을 할 때 훨씬 안정적이었어요. 사람도 그렇잖아요. “이 프로젝트 좀 잘 만들어봐”보다 “이 파일에서 이 함수만 고쳐줘”가 훨씬 일하기 쉽죠.
제가 실제로 잡아둔 DB 의존성 구조
제가 프로젝트에서 먼저 정리한 구조는 대략 이런 느낌이었습니다.
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
DATABASE_URL = "postgresql+asyncpg://user:password@localhost:5432/travel"
engine = create_async_engine(
DATABASE_URL,
echo=False,
pool_pre_ping=True
)
AsyncSessionLocal = async_sessionmaker(
bind=engine,
expire_on_commit=False
)
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
yield session
이 파일을 먼저 만들어두고 AI에게 말했습니다. “앞으로 DB 세션은 이 get_db를 Depends로 주입해서 써.” 이렇게 컨텍스트를 줬더니, 그다음부터는 결과물이 꽤 안정적으로 나왔습니다. 그냥 자유롭게 코딩하라고 하면 AI가 자기 방식대로 막 섞는데, 기준 파일을 하나 보여주면 그 틀 안에서 움직이더라고요.
| 작업 방식 | 결과 | 제 느낌 |
|---|---|---|
| 전체 CRUD를 한 번에 요청 | 빠르지만 구조가 뒤섞임 | 초안용으로는 괜찮지만 그대로 쓰긴 불안함 |
| DB 설정, 모델, 서비스, 라우터를 나눠서 요청 | 실수가 줄고 리뷰가 쉬움 | 실무에서는 이쪽이 훨씬 마음 편함 |
| 기준 코드 없이 요청 | AI가 임의의 스타일로 작성 | 나중에 통일감이 깨짐 |
| 기준 파일과 폴더 구조를 먼저 제공 | 프로젝트 스타일을 비교적 잘 따라감 | 이건 꽤 쓸 만했음 |
그리고 하나 더. FastAPI 버전도 꼭 말해주는 게 좋습니다. 이걸 안 말하면 AI가 예전 스타일 코드와 최신 스타일 코드를 섞어서 줄 때가 있어요. 저는 어느 순간부터 프롬프트에 “FastAPI 최신 버전 기준으로, 가능하면 Annotated 스타일을 사용해줘” 같은 문장을 넣었습니다. 그러면 결과물이 훨씬 요즘 코드에 가까워졌어요.
물론 최신 문법이 항상 정답은 아닙니다. 팀에서 이미 쓰는 스타일이 있다면 그게 더 중요하죠. 저는 회사 코드에서는 팀 컨벤션을 우선하고, 개인 프로젝트에서는 최신 스타일을 조금씩 실험해보는 편입니다. 나이 들수록 새 기술을 막 좇아가는 건 조금 피곤하지만, 그래도 아예 안 보면 금방 굳어버리더라고요. 개발자는 참 이상한 직업입니다. 쉬고 싶어도 계속 배워야 해요.
AI가 만든 코드는 빨리 돌아가지만, 오래 버티는지는 또 다른 문제예요
AI로 코딩해보면 초반 속도는 정말 빠릅니다. 이건 인정해야 해요. 예전 같으면 라우터 만들고, 스키마 만들고, 서비스 함수 만들고, 테스트용 요청까지 구성하느라 한두 시간은 훌쩍 갔을 작업이 10분 안에 얼추 나옵니다. 특히 반복적인 CRUD 작업에서는 꽤 든든합니다.
그런데 시간이 조금 지나고 기능이 늘어나면 슬슬 문제가 보입니다. AI는 그 순간의 요청을 해결하는 데 집중하지, 전체 시스템의 미래를 깊게 고민하지는 않거든요. 제가 겪은 대표적인 케이스가 여행지 검색 API였습니다.
처음에는 단순히 “여행지 목록 조회 API 만들어줘”라고 했습니다. 그랬더니 잘 만들어줬어요. 다음에는 “지역 필터 추가해줘”라고 했습니다. 또 잘 붙여줬습니다. 그다음엔 “태그 검색도 넣어줘”, “정렬 옵션도 넣어줘”, “페이지네이션도 넣어줘”라고 계속 시켰죠. 문제는 그렇게 몇 번 반복하고 나니, 엔드포인트 함수 하나가 200줄 가까이 되어 있었습니다.
작동은 합니다. 이게 참 사람을 헷갈리게 해요. 작동은 하는데 읽기 싫은 코드가 됩니다. 필터링, 정렬, 페이징, 예외 처리, 응답 포맷 변환이 한 함수 안에 다 들어가 있으니 나중에 조건 하나 추가하려고 해도 손이 덜덜 갑니다. 예전 SI 프로젝트에서 봤던 거대한 서비스 메서드가 떠오르더라고요. 아, 그 느낌 아시죠. 열면 한숨부터 나오는 파일.
그래서 저는 어느 순간 기준을 정했습니다. AI에게 아키텍처를 맡기지 말자. 대신 아키텍처는 내가 먼저 정하고, AI에게는 그 안에서 움직이게 하자. 이게 제일 안전했습니다.
제가 FastAPI 프로젝트에서 먼저 잡아둔 폴더 구조
my_travel_api/
├── app/
│ ├── routers/
│ │ ├── __init__.py
│ │ └── travels.py
│ ├── schemas/
│ │ └── travel.py
│ ├── services/
│ │ └── travel_service.py
│ ├── repositories/
│ │ └── travel_repository.py
│ ├── models/
│ │ └── travel.py
│ ├── common/
│ │ ├── dependencies.py
│ │ └── exceptions.py
│ └── main.py
이 구조를 AI에게 먼저 보여줬습니다. 그리고 이렇게 말했어요.
이 폴더 구조를 유지해줘.
라우터에는 요청과 응답 처리만 넣고,
비즈니스 로직은 services/travel_service.py에 넣어줘.
DB 접근은 repositories/travel_repository.py에서만 처리해줘.
이렇게 말하니까 코드 품질이 확 좋아졌습니다. 물론 완벽하진 않았지만, 적어도 한 함수에 모든 걸 욱여넣는 일은 줄어들었어요. AI는 생각보다 지시를 잘 따릅니다. 다만 내가 지시를 안 하면 자기 마음대로 합니다. 이건 사람하고도 비슷하네요.
| 레이어 | 역할 | AI에게 지시할 때 쓰기 좋은 표현 |
|---|---|---|
| routers | HTTP 요청, 응답, Depends 처리 | 라우터에는 비즈니스 로직을 넣지 말아줘 |
| schemas | 요청과 응답 모델 정의 | Pydantic 모델로 요청값과 응답값을 분리해줘 |
| services | 비즈니스 규칙 처리 | 필터 조건 조합은 service 레이어에서 처리해줘 |
| repositories | DB 쿼리 전담 | SQLAlchemy 쿼리는 repository에만 작성해줘 |
| common | 공통 의존성, 예외, 응답 포맷 | 공통 예외 처리는 common 모듈을 사용해줘 |
저는 개발하면서 종종 여행할 때랑 비슷하다는 생각을 합니다. 여행도 목적지만 찍고 무작정 가면 재미는 있는데, 숙소 위치나 이동 동선이 꼬이면 몸이 힘들잖아요. 코딩도 비슷합니다. AI가 빠르게 길을 찾아주긴 하는데, 전체 동선은 내가 봐야 합니다. 그래야 나중에 덜 헤맵니다.
성능 쪽은 더 조심해야 해요, AI가 async를 만능처럼 쓸 때가 있거든요
제가 FastAPI에서 AI를 쓰면서 특히 조심하게 된 부분이 성능입니다. AI는 async를 좋아합니다. 정말 좋아해요. 함수마다 async를 붙이고, 뭔가 비동기처럼 보이게 만들어줍니다. 그런데 모든 작업이 async에 어울리는 건 아니죠.
예를 들어 외부 API 호출이나 DB 조회처럼 기다리는 시간이 많은 작업은 async와 잘 맞습니다. 반면 이미지 리사이징, 대량 CSV 파싱, 복잡한 계산처럼 CPU를 많이 쓰는 작업은 async로 감싼다고 마법처럼 빨라지지 않습니다. 오히려 이벤트 루프를 막아서 전체 응답성이 나빠질 수도 있어요.
제가 테스트했던 기능 중에 여행지 대표 이미지를 리사이징해서 썸네일을 만드는 작업이 있었습니다. AI가 처음에는 이걸 FastAPI 엔드포인트 안에서 async 함수로 처리하도록 만들었어요. 그런데 부하를 조금만 줘도 응답 시간이 확 늘어났습니다. 이유는 단순했어요. 이미지 처리는 CPU 작업인데, 이걸 요청 흐름 안에서 붙잡고 있었던 거죠.
그래서 저는 이런 기준을 세웠습니다.
- DB 조회처럼 I/O 대기가 많은 작업은 async로 처리한다.
- 외부 API 호출도 httpx 같은 async 클라이언트를 쓴다.
- 이미지 처리, 대용량 파일 파싱, 복잡한 계산은 요청 흐름에서 분리한다.
- 간단한 후처리는 FastAPI의 BackgroundTasks를 검토한다.
- 무거운 작업은 Celery, RQ, 별도 worker 같은 구조로 뺀다.
AI에게도 이렇게 물어보는 게 좋았습니다.
이 함수가 I/O 바운드인지 CPU 바운드인지 판단해줘.
FastAPI 요청 흐름에서 직접 처리해도 괜찮은지,
BackgroundTasks나 worker로 분리하는 게 나은지도 같이 설명해줘.
이 질문을 던지면 AI가 적어도 성능 관점에서 한 번 더 생각한 답을 줍니다. 그냥 “코드 만들어줘”라고 할 때보다 훨씬 낫습니다. 물론 그 판단도 그대로 믿으면 안 됩니다. 그래도 리뷰할 포인트를 잡아주는 데는 도움이 됩니다.
제가 정리한 AI 바이브 코딩 실전 체크리스트
몇 번 삽질하고 나니 저만의 체크리스트가 생겼습니다. 대단한 건 아닌데, 이 정도만 챙겨도 AI가 만든 코드를 실무 코드에 조금 더 가깝게 가져갈 수 있더라고요.
- 프레임워크 버전을 먼저 말하기
예를 들면 “FastAPI 최신 버전 기준”, “SQLAlchemy 2.0 async 기준”처럼요. 버전을 안 말하면 옛날 코드가 섞일 수 있습니다. - 폴더 구조를 먼저 제공하기
AI에게 자유를 너무 많이 주면 구조가 흐트러집니다. 라우터, 서비스, repository, schema 위치를 먼저 알려주는 게 좋습니다. - 한 번에 전체를 맡기지 않기
CRUD 전체보다 POST 하나, GET 하나, service 함수 하나처럼 작게 나누는 편이 안정적입니다. - Pydantic 모델부터 만들기
요청과 응답 스키마를 먼저 잡아두면 API가 훨씬 단단해집니다. - async 코드는 꼭 다시 보기
AsyncSession과 sync engine이 섞이진 않았는지, async 함수 안에서 blocking 작업을 하고 있진 않은지 확인해야 합니다. - 테스트 코드를 같이 요청하기
API 코드만 받지 말고 pytest 예제까지 같이 받으면 놓친 부분이 빨리 보입니다. - 에러 메시지를 그대로 붙여넣기
AI에게 “안 돼요”라고 하지 말고, 실제 traceback을 붙여주면 해결 확률이 훨씬 높아집니다.
특히 에러 메시지를 그대로 주는 건 정말 효과가 좋았습니다. 예전에는 저도 괜히 상황을 설명하려고 길게 썼는데, 결국 AI도 로그를 봐야 정확히 알더라고요. 사람 개발자랑 똑같습니다. “뭔가 안 돼요”보다 에러 로그 한 줄이 훨씬 강합니다.
AI가 편하긴 한데, 그래도 개발자의 판단은 절대 빠지면 안 됩니다
요즘 가끔 이런 생각을 합니다. 앞으로 개발자는 코드를 직접 치는 사람이라기보다, 좋은 질문을 하고 결과를 검증하는 사람에 더 가까워지겠구나. 조금 서운하기도 하고, 한편으로는 재밌기도 합니다. 20년 전에는 IDE 자동완성만 좋아져도 신기했는데, 이제는 AI가 함수 단위로 코드를 뽑아주니까요. 세상 참 빠릅니다.
그런데 아무리 도구가 좋아져도 바뀌지 않는 게 있습니다. 좋은 코드는 여전히 읽기 쉬워야 하고, 테스트 가능해야 하고, 운영 중에 문제가 생겼을 때 추적 가능해야 합니다. AI가 만든 코드도 이 기준을 통과해야 합니다. “돌아가니까 됐다”는 말은 개인 토이 프로젝트에서는 괜찮지만, 누군가 쓰는 서비스에서는 조금 위험합니다.
제가 생각하는 AI 바이브 코딩의 가장 좋은 사용법은 이겁니다. AI에게 속도를 맡기되, 방향은 내가 잡는 것. 초안은 AI가 만들게 하되, 설계와 책임 분리는 내가 결정하는 것. 그렇게 쓰면 정말 편합니다. 반대로 AI가 만든 코드를 아무 검토 없이 계속 붙이면, 당장은 빠른데 나중에 내가 만든 코드인지 AI가 만든 덫인지 모를 상황이 옵니다.
이 글은 FastAPI를 막 시작한 개발자, AI 코딩 도구를 실무나 사이드 프로젝트에 써보려는 분, 그리고 저처럼 경력이 좀 있는데도 요즘 흐름에 맞춰 다시 손을 움직여보려는 분들이 읽으면 좋을 것 같습니다. 저도 아직 배우는 중입니다. 다만 한 가지는 확실히 말할 수 있어요. AI 바이브 코딩은 장난감이 아니라 꽤 쓸 만한 도구입니다. 대신 운전대는 여전히 사람이 잡아야 합니다.
오늘은 FastAPI로 AI 바이브 코딩을 해보면서 느낀 단맛과 쓴맛을 길게 풀어봤습니다. 사실 이런 얘기하다 보면 제가 너무 옛날 개발자처럼 보이나 싶기도 한데요. 그래도 뭐, 오래 개발하다 보면 조심성이 생기는 건 어쩔 수 없나 봅니다. 빠른 것도 좋지만, 오래 버티는 코드가 결국 사람을 덜 힘들게 하거든요. 여러분도 AI 잘 부려먹으시고, 코드는 꼭 한 번 더 의심해보세요. 그리고 가끔은 노트북 덮고 바람도 좀 쐬고요. 개발자는 숨 돌릴 때 코드가 더 잘 보이더라고요.
댓글
댓글 쓰기