콘텐츠로 이동

Ch.9 AI가 자주 틀리는 패턴

< 사례: AI가 자신 있게 틀린 코드들 | CS 관점 코드 리뷰 체크리스트 >


앞에서 세 가지 사례(동시성 무시, 성능 무시, 과도한 추상화)를 봤다. 이번에는 AI가 코드를 생성할 때 자주 범하는 실수를 패턴별로 정리한다. 이 패턴을 알고 있으면 AI 코드를 받았을 때 "어디를 봐야 하는지"를 바로 알 수 있다.

패턴 1: 동시성을 고려하지 않는다

AI가 생성하는 코드의 대부분은 "단일 요청, 단일 스레드" 기준이다. 동시에 여러 요청이 들어오는 상황을 기본적으로 고려하지 않는다.

확인해야 할 것:

  • 공유 자원(전역 변수, DB 레코드)에 읽기-수정-쓰기 패턴이 있는가?
  • 있다면 Lock이나 Atomic 연산으로 보호되고 있는가?
  • DB 작업이라면 적절한 Isolation Level이 설정되어 있는가?

Ch.5에서 배운 것: Race Condition은 "읽기와 쓰기 사이에 다른 스레드가 끼어들 수 있는가?"로 판단한다.

패턴 2: 데이터 규모를 고려하지 않는다

AI는 "동작하는 코드"를 만든다. 10건에서 잘 되는 코드를 준다. 그 코드가 10만 건, 100만 건에서도 잘 되는지는 알려주지 않는다.

확인해야 할 것:

  • 리스트를 전체 순회하는 로직이 있는가? (O(n) 또는 O(n^2))
  • 그 리스트의 크기가 얼마나 될 수 있는가?
  • 메모리에 전부 올려야 하는가, 아니면 DB에서 필터링할 수 있는가?
  • 중첩 반복문이 있는가? (O(n^2)의 신호)

Ch.4에서 배운 것: 메모리에 올리는 데이터가 커지면 Heap이 커지고, 결국 OOM이 난다. Ch.10에서 다루는 것: 자료구조 선택으로 O(n)을 O(1)로 바꿀 수 있다.

패턴 3: I/O 패턴을 잘못 선택한다

AI는 종종 "async로 바꾸면 빨라진다"고 제안하지만, CPU Bound 작업을 async로 바꾸면 오히려 느려진다. 반대로, I/O Bound 작업을 동기로 직렬 처리하는 코드를 만들기도 한다.

확인해야 할 것:

  • 이 작업은 CPU Bound인가, I/O Bound인가?
  • CPU Bound라면 async 대신 ProcessPoolExecutor를 써야 하는가?
  • I/O Bound라면 직렬 호출을 asyncio.gather로 병렬화할 수 있는가?
  • FastAPI에서 동기 DB 라이브러리를 async def로 쓰고 있지 않은가?

Ch.3에서 배운 것: CPU Bound는 멀티프로세스, I/O Bound는 async/await. Ch.6에서 배운 것: 동기 SQLAlchemy를 async def에서 쓰면 이벤트 루프가 블로킹된다.

패턴 4: 에러 핸들링이 과하거나 부족하다

AI는 두 극단 중 하나로 간다. 전부 try/except로 감싸서 에러를 삼켜버리거나, 에러 핸들링을 아예 안 하거나.

# AI가 자주 만드는 코드: 에러를 삼킨다
try:
    result = do_something()
except Exception:
    pass  # 무슨 에러인지도 모른 채 넘어간다

# 또는: 에러를 전부 같은 방식으로 처리한다
try:
    result = do_something()
except Exception as e:
    logger.error(f"에러: {e}")
    return {"error": "알 수 없는 에러"}

확인해야 할 것:

  • except Exception으로 모든 에러를 잡고 있지 않은가?
  • 잡아야 할 특정 에러(ConnectionError, TimeoutError)만 잡고 있는가?
  • 에러가 발생했을 때 Transaction이 제대로 롤백되는가?
  • 재시도가 필요한 에러(네트워크 일시 장애)와 불필요한 에러(잘못된 입력)를 구분하고 있는가?

패턴 5: 불필요한 추상화를 만든다

앞의 사례 C에서 봤다. AI는 디자인 패턴을 많이 학습했기 때문에, 물어보면 적용한다. Factory, Strategy, Observer, Abstract Class... 한 줄이면 되는 코드에도 패턴을 씌운다.

확인해야 할 것:

  • 이 추상화가 지금 당장 필요한가?
  • 구체 구현이 하나뿐인 인터페이스/추상 클래스가 있는가?
  • "나중에 확장할 수도 있으니까"가 유일한 이유인가?
  • 코드를 읽는 사람이 추상화 레이어를 따라가느라 더 힘들어지지 않는가?

Ch.20에서 다루는 것: 관심사의 분리는 필요하지만, 과도한 추상화는 오히려 유지보수를 어렵게 한다.

패턴 6: 보안을 고려하지 않는다

AI가 만든 코드에서 보안 취약점이 발견되는 경우가 있다. SQL Injection에 취약한 쿼리, 사용자 입력을 검증 없이 사용하는 코드, 비밀번호를 평문으로 저장하는 코드 등.

# AI가 만들 수 있는 취약한 코드
@app.get("/users/{user_id}")
def get_user(user_id: str):
    query = f"SELECT * FROM users WHERE id = '{user_id}'"  # SQL Injection
    return db.execute(query)

확인해야 할 것:

  • SQL 쿼리에 사용자 입력이 직접 들어가지 않는가? (Parameterized Query 사용)
  • 비밀번호가 평문으로 저장되거나 로그에 출력되지 않는가?
  • 사용자 입력이 HTML에 그대로 렌더링되지 않는가? (XSS)

Ch.23에서 다루는 것: OWASP Top 10, SQL Injection, XSS, CORS 등.

패턴 7: 잘못된 트랜잭션 경계

AI가 만든 서비스 코드에서 자주 보이는 게 "트랜잭션이 너무 좁거나 너무 넓은" 패턴이다.

# AI가 만들 수 있는 코드: 트랜잭션이 깨진다
def transfer_money(from_id: int, to_id: int, amount: int):
    with db.begin():
        from_account = db.query(Account).get(from_id)
        from_account.balance -= amount
    # 여기서 다른 요청이 끼어들면? 송금 중인 잔액으로 다른 트랜잭션 진행
    with db.begin():
        to_account = db.query(Account).get(to_id)
        to_account.balance += amount

송금이라는 하나의 비즈니스 단위가 두 개의 트랜잭션으로 쪼개졌다. 두 번째 트랜잭션 직전에 서버가 죽으면 돈이 사라진다. 반대 패턴도 있다.

# 또 다른 안티패턴: 트랜잭션 안에서 외부 API 호출
def create_order(item_id: int):
    with db.begin():
        order = Order(item_id=item_id)
        db.add(order)
        send_email(...)        # 외부 API. 1~2초 걸림
        notify_slack(...)      # 외부 API. 또 1~2초
        # 이 동안 DB Lock을 잡고 있다

확인해야 할 것:

  • 비즈니스 단위 하나가 하나의 트랜잭션으로 묶여 있는가?
  • 트랜잭션 안에 외부 API 호출, 파일 I/O, 긴 연산이 들어가 있지 않은가?
  • 트랜잭션 실패 시 보상 로직(rollback 외에 추가 정리)이 필요한 경우, 그 처리가 있는가?

Ch.15에서 Transaction과 Isolation Level을 다루고, Ch.16에서 Connection Pool 고갈 사례를 다룬다. 트랜잭션이 길면 그동안 Connection을 점유하기 때문에 Pool이 빨리 마른다.

패턴 8: Magic Number와 Magic String 남발

AI가 만든 코드에서 자주 보이는 또 다른 패턴이다.

# AI가 만들 수 있는 코드
def process_user(user):
    if user.status == 1:           # 1이 뭔가?
        user.score += 100          # 왜 100인가?
        if user.score > 1000:      # 1000은 무슨 임계값인가?
            user.status = 2        # 2는 뭔가?
            send_notification(user, type=3)  # 3이 뭐길래?

코드는 동작한다. 하지만 6개월 뒤에 본인도 못 읽는다. 협업하는 동료는 매번 1, 2, 3, 100, 1000의 의미를 추측해야 한다. 더 위험한 건 같은 상수가 여러 파일에 흩어졌을 때다. 한 곳에서 1000을 1500으로 바꾸고 다른 곳을 까먹으면 사일런트 버그가 된다.

# Enum과 상수로 의미를 명시
class UserStatus(IntEnum):
    PENDING = 1
    ACTIVE = 2

ACTIVATION_SCORE_BONUS = 100
ACTIVATION_THRESHOLD = 1000

def process_user(user):
    if user.status == UserStatus.PENDING:
        user.score += ACTIVATION_SCORE_BONUS
        if user.score > ACTIVATION_THRESHOLD:
            user.status = UserStatus.ACTIVE
            send_notification(user, type=NotificationType.ACTIVATION)

확인해야 할 것:

  • 숫자/문자열 리터럴이 코드에 직접 들어가 있지 않은가?
  • 같은 상수가 여러 파일에 중복되어 있지 않은가?
  • 상태 값은 Enum으로, 설정 값은 상수 또는 설정 파일로 빠져 있는가?

패턴 9: 단일 스레드 가정의 캐시/카운터

AI는 종종 클래스 변수나 모듈 전역 변수에 상태를 들고 있는 코드를 만든다. 싱글톤이 아니더라도 "프로세스 안에서 공유되는 상태"가 자주 등장한다.

# AI가 만들 수 있는 코드: 단순 메모리 카운터
class RateLimiter:
    _counters: dict[str, int] = {}

    def hit(self, key: str) -> int:
        self._counters[key] = self._counters.get(key, 0) + 1
        return self._counters[key]

단일 프로세스, 단일 스레드 환경에서는 잘 동작한다. 그런데 uvicorn을 --workers 4로 띄우면? 4개의 프로세스가 각자의 _counters를 들고 있기 때문에 Rate Limiting이 정확히 1/4로 깨진다. 멀티스레드 환경에서는 dict 갱신 자체가 Race Condition을 일으킨다.

확인해야 할 것:

  • 프로세스 안에서만 유효한 상태(전역 dict, 인스턴스 캐시)에 의존하지 않는가?
  • 멀티 워커/멀티 인스턴스 환경에서도 정합성이 깨지지 않는가?
  • 카운터/Rate Limit 같은 공유 상태는 Redis 같은 외부 저장소로 빠져 있는가?

Ch.5(동시성), Ch.17(Redis 캐시)에서 자세히 다룬다.

패턴 10: 로깅과 관측성의 누락

AI는 "기능"을 만들지만, "운영에서 문제가 생겼을 때 추적할 단서"는 잘 안 만든다.

# AI가 만들 수 있는 코드
def process_payment(order_id: int, amount: int):
    try:
        result = payment_gateway.charge(amount)
        if result.success:
            return {"status": "ok"}
        return {"status": "failed"}
    except Exception:
        return {"status": "error"}

운영에서 결제 실패가 5% 발생한다고 가정하자. 이 코드는 어디서 왜 실패했는지 단서를 전혀 남기지 않는다. 어떤 사용자가, 어떤 주문에서, 어떤 PG 응답 코드를 받았는지 모른다.

확인해야 할 것:

  • 외부 시스템 호출 결과(성공/실패, 응답 코드)를 로깅하는가?
  • 로그에 추적 가능한 식별자(request_id, order_id 등)가 포함되는가?
  • 비정상 케이스에서 어떤 데이터를 봤는지 후행 분석할 수 있게 되어 있는가?
  • 메트릭(요청 수, 실패율, 지연 시간)을 노출할 수 있는 구조인가?

AI 코드를 받을 때의 자세

이 패턴들을 정리하면, AI 코드를 리뷰할 때의 자세가 보인다.

"AI는 기능은 잘 만든다. 하지만 동시성, 성능, 보안, 트랜잭션 경계, 멀티 프로세스, 관측성, 적절성은 사람이 판단해야 한다."

각 패턴은 결국 "AI 훈련 데이터에 가장 자주 등장한 정답"을 따른 결과다. 그 정답이 통하지 않는 운영 환경의 경계 조건(동시 요청, 큰 데이터, 멀티 워커, 장애 추적)을 챙기는 게 리뷰어의 역할이다.

구체적인 체크리스트는 다음에서 정리한다.


< 사례: AI가 자신 있게 틀린 코드들 | CS 관점 코드 리뷰 체크리스트 >