Ch.8 사례: 같은 문제, 다른 카테고리의 키워드¶
Ch.7에서 "키워드가 프롬프트의 방향을 결정한다"고 했다. 이번에는 한 걸음 더 나간다. 같은 문제라도 어느 카테고리의 키워드를 쓰느냐에 따라 AI의 해법이 완전히 달라진다.
증상은 보통 한 줄이다. "API가 느려요", "서버가 죽어요", "DB가 멈춰요". 그런데 진짜 원인은 OS일 수도, DB일 수도, 네트워크일 수도, 자료구조일 수도, 아키텍처일 수도 있다. 어느 카테고리에서 출발하느냐가 진단의 절반이다.
8-1. 사례: "사용자 조회 API가 느리다"¶
상황은 단순하다. GET /users/{id} API가 평균 800ms, p99 2초다. 사용자 한 건 조회하는 데 너무 오래 걸린다. 이 한 줄짜리 증상을 5명의 개발자가 각자 다른 카테고리의 키워드로 진단해본다.
개발자 A: OS 키워드로 접근¶
[프롬프트]
GET /users/{id} API가 느리다. CPU는 여유 있는데 응답이 800ms다.
strace로 따라가니 write() system call이 요청당 수백 번 호출되고 있다.
컨트롤러에서 print/log를 한 줄씩 쓰고 있어서 stdout이 병목인 것 같다.
1. 로그 레벨을 조정해줘 (DEBUG 제거)
2. 남길 로그는 QueueHandler 기반 비동기 로깅으로 바꿔줘
AI 응답 방향:
import logging
from logging.handlers import QueueHandler, QueueListener
from queue import Queue
log_queue = Queue(-1)
handler = QueueHandler(log_queue)
listener = QueueListener(log_queue, logging.FileHandler("app.log"))
listener.start()
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.addHandler(handler)
Ch.2의 "System Call이 비싸다"가 진단의 출발점이었다. 키워드가 write(), stdout, QueueHandler였기 때문에 AI는 바로 비동기 로깅 코드를 줬다.
개발자 B: 네트워크 키워드로 접근¶
[프롬프트]
GET /users/{id} API가 느리다. 응답이 800ms인데 그 중 700ms가 외부 호출이다.
프로필 이미지 서버, 권한 서버, 활동 로그 서버를 순차 호출한다.
각각 RTT 200ms 정도. 직렬이라 700ms 가까이 걸린다.
asyncio + aiohttp로 3개를 병렬 호출해서 max(RTT) 수준으로 줄이고 싶다.
AI 응답 방향:
import asyncio
import aiohttp
async def get_user_full(user_id: int):
async with aiohttp.ClientSession() as session:
profile, perm, activity = await asyncio.gather(
session.get(f"/profile/{user_id}"),
session.get(f"/permission/{user_id}"),
session.get(f"/activity/{user_id}"),
)
return await combine(profile, perm, activity)
Ch.6의 "Connection 비용"과 Ch.3의 "I/O Bound는 async가 답"이 진단의 출발점이었다.
개발자 C: DB 키워드로 접근¶
[프롬프트]
GET /users/{id} API가 느리다. SQLAlchemy 로그를 켰더니
유저 1건 조회하는데 같은 SELECT가 N번 반복된다.
users → orders → items 순으로 lazy loading이 걸려서 N+1이 되고 있다.
EXPLAIN으로 보니 Full Table Scan도 한 번 있다.
1. joinedload/selectinload로 eager loading 적용
2. orders.user_id에 인덱스 걸기
AI 응답 방향:
from sqlalchemy.orm import selectinload
def get_user(session, user_id: int):
return session.query(User).options(
selectinload(User.orders).selectinload(Order.items),
).filter(User.id == user_id).one()
Ch.13에서 다룰 "N+1 쿼리"와 Ch.14의 "인덱스"가 진단의 출발점이었다.
개발자 D: 자료구조 키워드로 접근¶
[프롬프트]
GET /users/{id} API가 느리다. DB는 빠른데(10ms) 애플리케이션에서 500ms가 더 걸린다.
프로파일링 해보니 권한 체크 로직이 user.permissions 리스트에서
required_perms = ["read", "write", ...] 약 50개를 in 연산으로 확인하고 있다.
user.permissions 자체도 수백 개라 매 요청마다 O(n*m).
1. user.permissions를 set으로 바꿔서 O(1) 조회로
2. 가능하면 required_perms도 frozenset으로
AI 응답 방향:
class User:
@property
def permission_set(self) -> frozenset[str]:
return frozenset(self.permissions)
REQUIRED_PERMS = frozenset({"read", "write", "delete"})
def has_required(user: User) -> bool:
return REQUIRED_PERMS.issubset(user.permission_set)
Ch.10의 "List vs Set 시간 복잡도"가 진단의 출발점이었다.
개발자 E: 아키텍처 키워드로 접근¶
[프롬프트]
GET /users/{id} API가 느리다. 단일 모놀리스에서
유저 정보 + 결제 이력 + 추천 상품을 한 번에 내려주고 있다.
유저 모듈은 빠른데, 결제와 추천이 무거워서 전체 응답이 따라 느려진다.
1. 추천은 Read 비중이 압도적이라 CQRS로 Read Replica에서 조회
2. 결제 이력 알림은 Event-Driven으로 분리, 메인 응답에서 제외
3. 외부 호출은 Circuit Breaker로 묶어서 부분 장애가 전체를 죽이지 않게
AI 응답 방향: 추천 서비스 Read Replica 분리 설계, 결제 이벤트 발행 코드 스켈레톤(OrderCreated → Message Queue), Circuit Breaker 라이브러리(pybreaker, tenacity) 적용 예시.
Ch.22의 "관심사 분리"와 Ch.19의 "Read/Write 분리"가 진단의 출발점이었다.
8-2. 다섯 접근의 비교¶
| 접근 | 진단 도구 | 핵심 키워드 | 해법 | 관련 챕터 |
|---|---|---|---|---|
| OS | strace, htop | System Call, write(), stdout, QueueHandler | 로그 레벨 조정, 비동기 로깅 | Ch.2 |
| Network | 외부 호출 시간 측정, RTT | RTT, Connection Pool, asyncio.gather | 병렬 호출 | Ch.6 |
| DB | ORM 쿼리 로그, EXPLAIN | N+1, eager loading, Index | joinedload/selectinload, CREATE INDEX | Ch.13~14 |
| 자료구조 | 프로파일러 (cProfile) | List vs Set, Time Complexity, O(n) | List → Set/frozenset | Ch.10 |
| 아키텍처 | 모듈별 응답 분해 | Monolith, CQRS, Event-Driven, Circuit Breaker | 읽기 분리, 비동기 이벤트 | Ch.19~22 |
다섯 명 다 "GET /users/{id}가 느리다"는 동일한 한 줄에서 출발했다. 그런데 진단 결과가 다르고, 쓴 키워드가 다르고, AI의 해법이 다르다. 어느 게 정답인가? 전부 정답이다. 원인이 다르니까.
(현실에서는 한 명이 다 했을 가능성이 높다. "처음엔 DB부터 보고, 그게 아니면 외부 호출 보고…" 하는 순서로. 그게 가능하려면 5개 카테고리의 키워드를 다 알아야 한다는 뜻이다.)
중요한 건 두 가지다.
- 어느 카테고리의 키워드를 꺼낼 수 있느냐가 진단 능력이다
- 정확한 키워드 없이 AI에게 던지면, AI는 "엉뚱한 카테고리"의 답을 줄 가능성이 높다
8-3. 키워드 없이 같은 문제를 물어보면¶
비교를 위해 똑같은 문제를 키워드 없이 물어본 케이스를 보자.
AI 응답 방향(요약):
문제는 두 가지다.
- 다섯 가지 일반론을 늘어놓는다. 그 중 진짜 원인에 해당하는 건 운이 좋아야 한 개다
- "왜 그렇게 해야 하는가"의 진단이 빠져 있다. 인덱스가 문제인지, 외부 호출이 문제인지, 권한 체크 O(n*m)이 문제인지 모른 채로 다 시도하게 된다
"느린데 어떻게 해야 하나"라고 물으면 AI는 "느릴 때 흔히 시도하는 것"을 늘어놓는다. Ch.7에서 본 "키워드 없는 프롬프트 → 평균적 답변"의 전형적인 모습이다.
반면 앞에서 본 다섯 개발자의 프롬프트에는 전부 진단 결과가 들어 있다. "write() 호출이 많다", "RTT 200ms 외부 호출 3개", "N+1과 Full Table Scan", "권한 체크 O(n*m)", "결제/추천 모듈이 무겁다". AI는 그 진단에 맞는 정확한 코드를 생성한다.
8-4. 다음 챕터에서 다루는 것¶
다음부터 카테고리별로 "AI에게 줄 수 있는 핵심 키워드"를 정리한다. 각 키워드가 어떤 상황에서, 어떤 프롬프트에 쓰이는지를 보여주는 게 이 챕터의 목적이다.
graph LR
SYM["증상 한 줄<br/>(느리다/죽는다/꼬인다)"] --> CAT{"어느 카테고리?"}
CAT --> OS["OS"]
CAT --> NET["네트워크"]
CAT --> DB["DB"]
CAT --> DS["자료구조"]
CAT --> ARCH["아키텍처"]
OS --> P["카테고리 키워드<br/>+ 진단 결과<br/>= 좋은 프롬프트"]
NET --> P
DB --> P
DS --> P
ARCH --> P
전부 외울 필요는 없다. "이런 키워드가 있다"는 걸 알아두면, 문제가 생겼을 때 꺼내 쓸 수 있다. 카테고리 선택이 진단의 출발점이다.