AI 기반 API 래핑으로 레거시 시스템 현대화하기
1.1 어느 개발자의 절망적인 오후 3시#
오후 3시.
개발팀의 박 대리는 모니터를 바라보며 깊은 한숨을 내쉬었다.
화면에는 새로 개발한 반짝이는 프론트엔드와, 그 아래에서 15년째 묵묵히 돌아가고 있는 재고 관리 시스템의 API 응답이 나란히 떠 있었다.
문제는 프론트엔드가 아니었다.
진짜 문제는 이것이었다.
{
"RESULT": "SUCCESS",
"DATA": "P0012^에어컨 필터^삼성^25000^재고있음^창고A-3-2"
}이것만 보면 그럭저럭 처리할 수 있을 것 같았다.
하지만 다음 요청에서는 전혀 다른 형태가 등장했다.
D-101|세탁기|LG|1,200,000원|재고보유|부산물류센터 2층또 다른 요청은 이랬다.
X-998/노트북/Apple/가격문의/주문가능/본사같은 API인데 데이터 구분자가 ^, |, /, , 등 제각각이었다.
가격도 문제였다.
25000
1,200,000원
가격문의
이십오만원재고 상태 역시 일정하지 않았다.
재고있음
재고보유
주문가능
재고없음
품절박 대리가 해야 할 일은 간단했다.
최신 서비스가 이해할 수 있는 다음과 같은 JSON을 만들어주는 것이다.
{
"product_id": "P0012",
"product_name": "에어컨 필터",
"brand": "삼성",
"price": 25000,
"in_stock": true,
"location": "창고A-3-2"
}그런데 현실에서는 split()이 등장했다.
그리고 if가 등장했다.
그다음에는 또 다른 if가 등장했다.
if "^" in data:
parts = data.split("^")
elif "|" in data:
parts = data.split("|")
elif "/" in data:
parts = data.split("/")가격도 별도로 처리해야 했다.
if "원" in price:
price = price.replace("원", "")
if "," in price:
price = price.replace(",", "")그리고 예외 처리가 추가됐다.
try:
...
except Exception:
...시간이 지나자 원래 몇 줄이면 끝났어야 할 API 연동 코드가 수백 줄로 늘어났다.
팀장은 말했다.
"이 API는 건드리지 마세요. 이거 사용하는 시스템이 한두 개가 아닙니다. 그냥 박 대리님이 잘 파싱해서 쓰세요."
틀린 말은 아니었다.
15년 된 레거시 시스템은 회사의 여러 시스템과 연결되어 있었다.
재고 시스템은 영업 시스템과 연결되어 있었고, 영업 시스템은 회계 시스템과 연결되어 있었으며, 물류 시스템 역시 재고 시스템을 바라보고 있었다.
이런 상황에서 레거시 API의 응답 형식을 바꾸는 것은 단순한 코드 수정이 아니다.
잘못 건드리면 연쇄적인 장애로 이어질 수 있다.
그래서 개발팀은 새로운 서비스를 만들면서도 오래된 시스템을 그대로 두고 그 주변에서 문제를 해결해야 했다.
이것이 바로 레거시 시스템 현대화에서 자주 마주치는 현실이다.
1.2 레거시 시스템을 없애지 않고 활용하는 방법#
레거시 시스템을 만났을 때 개발자가 선택할 수 있는 방법은 크게 세 가지다.
첫 번째는 레거시 시스템을 직접 수정하는 것이다.
두 번째는 새로운 시스템으로 완전히 교체하는 것이다.
세 번째는 기존 시스템을 그대로 둔 채 그 앞에 새로운 계층을 추가하는 것이다.
현실적인 기업 환경에서는 세 번째 방법이 상당히 중요하다.
레거시 시스템을 당장 제거할 수 없다면 새로운 시스템과 레거시 시스템 사이에 경계층을 만드는 것이다.
[새로운 서비스]
│
▼
[현대적인 API]
│
▼
[AI 기반 래퍼]
│
▼
[레거시 API]
│
▼
[오래된 데이터베이스]이 구조에서 새로운 서비스는 레거시 시스템의 복잡한 사정을 알 필요가 없다.
새로운 서비스는 오직 현대적인 API만 바라본다.
레거시 시스템의 이상한 응답 형식이나 오래된 인증 방식, 필드 이름, 데이터 표현 방식은 래퍼 내부에 숨겨진다.
이것이 API 래핑의 기본적인 아이디어다.
1.3 API 래핑이란 무엇인가#
API 래핑은 복잡한 API를 다른 프로그램이 사용하기 쉬운 형태로 감싸는 기술이다.
가장 쉬운 예는 데이터 형식 변환이다.
XML API
│
▼
[API Wrapper]
│
▼
JSON API또는 프로토콜을 변환할 수도 있다.
SOAP
│
▼
[Wrapper]
│
▼
REST인증 방식도 통일할 수 있다.
서비스 A ─┐
서비스 B ─┼─▶ [API Wrapper] ─▶ 레거시 시스템
서비스 C ─┘기존 API 래퍼는 대부분 규칙 기반 변환이었다.
예를 들어 XML의 특정 태그를 JSON의 특정 필드로 바꾸는 식이다.
<product>
<id>P0012</id>
<name>에어컨 필터</name>
<price>25000</price>
</product>다음과 같이 바꿀 수 있다.
{
"productId": "P0012",
"productName": "에어컨 필터",
"price": 25000
}이런 작업은 매우 안정적이다.
문제는 입력이 규칙에서 벗어나기 시작할 때다.
1.4 규칙 기반 래퍼의 한계#
다음과 같은 데이터가 있다고 생각해보자.
P0012^에어컨 필터^삼성^25000^재고있음^창고A-3-2규칙 기반 코드라면 쉽게 처리할 수 있다.
parts = raw_data.split("^")
product_id = parts[0]
product_name = parts[1]
brand = parts[2]
price = int(parts[3])하지만 데이터가 다음처럼 바뀌면 이야기가 달라진다.
D-101|세탁기|LG|1,200,000원|재고보유|부산물류센터 2층이번에는 |로 나눠야 한다.
가격도 숫자가 아니다.
1,200,000원또 다른 데이터는 다음과 같다.
X-998/노트북/Apple/가격문의/주문가능/본사가격 자체가 없다.
그리고 구분자만으로 모든 문제를 해결할 수 없는 경우도 있다.
예를 들어 주소에 구분자가 포함될 수 있다.
P0013|에어컨|LG|서울 강남구 테헤란로 123|재고있음어떤 시스템에서는 구분자가 데이터 내부에서도 사용된다.
이 순간부터 단순한 split()은 점점 복잡한 예외 처리 코드로 변한다.
여기에서 AI가 새로운 역할을 할 수 있다.
1.5 AI 기반 API 래핑이란 무엇인가#
AI 기반 API 래핑은 기존 API의 데이터를 단순한 규칙 변환만으로 처리하지 않고 AI가 데이터의 의미와 문맥을 해석하도록 만드는 방식이다.
기존 방식은 다음과 같다.
원본 데이터
│
▼
[split / replace / if]
│
▼
정형 데이터AI 기반 방식은 다음과 같다.
원본 데이터
│
▼
[AI 해석]
│
▼
[구조화된 출력]
│
▼
[Pydantic 검증]
│
▼
정형 API 응답예를 들어 AI에게 다음과 같은 데이터를 전달할 수 있다.
D-101|세탁기|LG|1,200,000원|재고보유|부산물류센터 2층그리고 원하는 구조를 정의한다.
{
"product_id": "D-101",
"product_name": "세탁기",
"brand": "LG",
"price": 1200000,
"in_stock": true,
"location": "부산물류센터 2층"
}AI는 단순히 문자열을 잘라내는 것이 아니라 각 값이 어떤 의미를 갖는지 해석할 수 있다.
이것이 AI 래핑의 핵심적인 차이다.
다만 여기서 중요한 원칙이 있다.
AI는 정규식이나 문자열 처리의 대체품이 아니다.
입력이 항상 일정하고 규칙이 명확하다면 일반적인 코드가 더 빠르고 저렴하며 예측 가능하다.
AI를 사용하는 것이 의미가 있는 지점은 입력 형식이 다양하거나, 사람이 작성한 텍스트처럼 의미 해석이 필요한 경우다.
1.6 AI 래퍼의 기본 구조#
AI 기반 API 래퍼는 일반적으로 다음과 같은 구조로 설계할 수 있다.
┌─────────────────┐
│ 신규 서비스 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 현대적 API │
│ /v1/stock/{id} │
└────────┬────────┘
│
▼
┌───────────────────────┐
│ AI API Wrapper │
│ │
│ ① 레거시 호출 │
│ ② 데이터 정제 │
│ ③ 구조화 출력 │
│ ④ 스키마 검증 │
└───────────┬───────────┘
│
▼
┌─────────────────┐
│ 레거시 API │
└─────────────────┘이 구조에서 중요한 것은 AI가 시스템 전체를 통제하는 것이 아니라는 점이다.
AI는 경계층에서 데이터를 해석하는 역할을 담당한다.
비즈니스 로직 전체를 LLM에게 맡기는 것이 아니다.
1.7 실전 프로젝트 구성#
이번 예제에서는 다음과 같은 기술을 사용한다.
Python
FastAPI
Pydantic
httpx
OpenAI API
python-dotenv프로젝트 구조는 다음과 같이 구성한다.
ai-api-wrapper/
├── main.py
├── models.py
├── ai_parser.py
├── legacy_mock.py
├── .env
└── requirements.txt필요한 패키지를 설치한다.
pip install fastapi uvicorn httpx openai python-dotenv pydantic환경 변수에는 API 키를 저장한다.
OPENAI_API_KEY=your_api_keyAPI 키를 소스 코드에 직접 작성하는 것은 피해야 한다.
1.8 먼저 최종 데이터 구조를 정의한다#
AI를 호출하기 전에 먼저 우리가 원하는 결과의 형태를 정의해야 한다.
여기에서는 Pydantic을 사용한다.
models.py
from pydantic import BaseModel, Field
class ProductStock(BaseModel):
product_id: str = Field(
description="제품의 고유 식별자"
)
product_name: str = Field(
description="제품 이름"
)
brand: str = Field(
description="제조사 또는 브랜드"
)
price: int | None = Field(
default=None,
description="가격. 숫자로 변환할 수 없으면 null"
)
in_stock: bool = Field(
description="재고 보유 여부"
)
location: str = Field(
description="재고 위치"
)이 모델은 단순한 데이터 클래스가 아니다.
AI가 생성한 결과를 검증하는 계약으로 사용할 수 있다.
FastAPI 역시 Pydantic 기반 응답 모델을 사용해 반환 데이터의 형태를 검증하고 OpenAPI 문서에도 스키마를 반영할 수 있다.
1.9 레거시 API를 만들어보자#
실제 회사의 레거시 시스템을 실습 환경에서 계속 호출할 수는 없다.
따라서 먼저 레거시 시스템을 흉내 내는 Mock API를 만든다.
legacy_mock.py
from fastapi import FastAPI, HTTPException
app = FastAPI()
mock_responses = {
"P0012": "P0012^에어컨 필터^삼성^25000^재고있음^창고A-3-2",
"D-101": "D-101|세탁기|LG|1,200,000원|재고보유|부산물류센터 2층",
"X-998": "X-998/노트북/Apple/가격문의/주문가능/본사",
"Z-001": "Z-001,스마트폰,Samsung,990000,품절,온라인 전용",
}
@app.get("/legacy/stock/{product_id}")
async def get_legacy_stock(product_id: str):
if product_id not in mock_responses:
raise HTTPException(
status_code=404,
detail="Product not found in legacy system",
)
return {
"RESULT": "SUCCESS",
"DATA": mock_responses[product_id],
}이 API가 반환하는 데이터는 우리가 원하는 최신 JSON API와는 거리가 멀다.
하지만 이것이 바로 우리가 해결하려는 문제다.
1.10 AI에게 비정형 데이터를 구조화시키기#
이제 AI 파서를 만든다.
여기서 중요한 변화가 있다.
예전 방식의 예제에서는 AI에게 JSON을 만들어 달라고 요청한 뒤 json.loads()로 파싱하고 Pydantic으로 검증했다.
현재는 구조화된 출력과 명시적인 JSON Schema를 사용하는 방식이 더 적합하다. OpenAI API 문서에서도 기존 json_object 방식보다 json_schema 기반 Structured Outputs 사용을 권장하고 있다.
예제는 다음과 같이 구성할 수 있다.
ai_parser.py
import os
from dotenv import load_dotenv
from openai import OpenAI
from models import ProductStock
load_dotenv()
client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY")
)
def parse_legacy_stock_data(raw_data: str) -> ProductStock:
prompt = f"""
레거시 재고 시스템에서 전달된 원본 데이터를
ProductStock 구조로 변환하세요.
변환 규칙:
1. product_id는 제품 식별자입니다.
2. product_name은 제품 이름입니다.
3. brand는 제조사 또는 브랜드입니다.
4. price는 숫자만 반환합니다.
5. 가격을 확인할 수 없다면 null을 사용합니다.
6. 쉼표와 통화 단위는 제거합니다.
7. in_stock은 재고가 있거나 주문 가능한 상태이면 true입니다.
8. 품절 또는 재고가 없는 상태이면 false입니다.
9. location은 재고 위치입니다.
10. 데이터의 구분자는 고정되어 있지 않을 수 있습니다.
11. 원본에 없는 값을 임의로 만들어내지 마세요.
원본 데이터:
{raw_data}
"""
response = client.responses.parse(
model="gpt-5",
input=prompt,
text_format=ProductStock,
)
return response.output_parsedOpenAI의 최신 Python 사용 방식에서는 responses.create() 같은 Responses API 기반 호출이 사용되며, 공식 퀵스타트에서도 이를 기본적인 API 호출 방식으로 소개하고 있다.
또한 구조화된 출력을 사용할 경우 단순히 "JSON으로 만들어주세요"라고 요청하는 것보다 정해진 스키마를 모델에 제공하고 그 구조를 강제하는 방식이 더 적합하다.
1.11 왜 Pydantic이 중요한가#
여기에서 중요한 것은 AI를 믿는 것이 아니다.
오히려 반대다.
AI의 출력을 검증해야 한다.
LLM은 아무리 강력해도 데이터베이스나 타입 시스템 자체가 아니다.
예를 들어 우리가 원하는 데이터가 다음과 같다고 하자.
{
"product_id": "P0012",
"product_name": "에어컨 필터",
"brand": "삼성",
"price": 25000,
"in_stock": true,
"location": "창고A-3-2"
}그런데 AI가 다음과 같은 값을 반환했다면 문제가 된다.
{
"product_id": "P0012",
"price": "약 25,000원",
"in_stock": "아마 있음"
}따라서 다음과 같은 구조가 필요하다.
레거시 데이터
│
▼
AI
│
▼
구조화된 출력
│
▼
Pydantic 검증
│
├── 성공 ──▶ API 응답
│
└── 실패 ──▶ 재시도 / 오류 처리FastAPI의 응답 모델 역시 반환 데이터의 검증과 직렬화, OpenAPI 스키마 생성에 활용된다.
즉 AI와 타입 검증을 결합하는 것이 핵심이다.
1.12 AI 래퍼 API 만들기#
이제 실제 외부에서 호출할 API를 만든다.
main.py
from fastapi import FastAPI, HTTPException
import httpx
from models import ProductStock
from ai_parser import parse_legacy_stock_data
app = FastAPI(
title="AI 기반 레거시 재고 API",
description="레거시 재고 API를 현대적인 구조화 API로 변환합니다.",
)
LEGACY_API_BASE_URL = "http://127.0.0.1:8001"
@app.get(
"/v1/stock/{product_id}",
response_model=ProductStock,
)
async def get_modern_stock(product_id: str):
async with httpx.AsyncClient(
timeout=5.0
) as client:
try:
response = await client.get(
f"{LEGACY_API_BASE_URL}/legacy/stock/{product_id}"
)
response.raise_for_status()
legacy_response = response.json()
raw_data = legacy_response.get("DATA")
if not raw_data:
raise HTTPException(
status_code=502,
detail="레거시 API 응답에 DATA가 없습니다.",
)
except httpx.HTTPStatusError as e:
raise HTTPException(
status_code=502,
detail="레거시 시스템 호출에 실패했습니다.",
) from e
except httpx.RequestError as e:
raise HTTPException(
status_code=502,
detail="레거시 시스템에 연결할 수 없습니다.",
) from e
try:
cleaned_data = parse_legacy_stock_data(
raw_data
)
except Exception as e:
raise HTTPException(
status_code=502,
detail="레거시 데이터를 구조화하는 데 실패했습니다.",
) from e
return cleaned_data여기에서 response_model=ProductStock을 사용하는 이유도 중요하다.
FastAPI는 응답 모델을 이용해 반환 데이터의 구조를 검증하고, API 문서의 JSON Schema에도 이를 반영한다.
1.13 실제 실행해보기#
터미널을 두 개 연다.
첫 번째 터미널에서 레거시 Mock API를 실행한다.
uvicorn legacy_mock:app --port 8001두 번째 터미널에서 AI 래퍼 API를 실행한다.
uvicorn main:app --port 8000그리고 브라우저에서 다음 주소로 접속한다.
http://127.0.0.1:8000/docsFastAPI가 자동으로 제공하는 API 문서에서 엔드포인트를 테스트할 수 있다.
1.14 첫 번째 데이터 변환#
다음 API를 호출한다.
레거시 시스템은 다음과 같은 데이터를 반환한다.
P0012^에어컨 필터^삼성^25000^재고있음^창고A-3-2AI 래퍼는 이를 다음과 같이 구조화한다.
{
"product_id": "P0012",
"product_name": "에어컨 필터",
"brand": "삼성",
"price": 25000,
"in_stock": true,
"location": "창고A-3-2"
}새로운 서비스는 레거시 시스템의 구분자가 ^였다는 사실을 알 필요가 없다.
1.15 두 번째 데이터 변환#
이번에는 다음 요청을 보낸다.
레거시 데이터는 다음과 같다.
D-101|세탁기|LG|1,200,000원|재고보유|부산물류센터 2층최종 결과는 다음과 같다.
{
"product_id": "D-101",
"product_name": "세탁기",
"brand": "LG",
"price": 1200000,
"in_stock": true,
"location": "부산물류센터 2층"
}이번에는 | 구분자를 사용했고 가격에는 쉼표와 원이 포함되어 있다.
하지만 새로운 API의 사용자는 이런 차이를 알 필요가 없다.
1.16 세 번째 데이터에서는 빈 값도 처리한다#
다음 요청을 실행한다.
레거시 데이터는 다음과 같다.
X-998/노트북/Apple/가격문의/주문가능/본사가격이 존재하지 않는다.
이 경우 올바른 결과는 다음과 같다.
{
"product_id": "X-998",
"product_name": "노트북",
"brand": "Apple",
"price": null,
"in_stock": true,
"location": "본사"
}여기에서 중요한 것은 AI가 없는 정보를 만들어내지 않는 것이다.
가격문의를 보고 임의로 가격을 추측해서는 안 된다.
따라서 프롬프트에도 다음과 같은 규칙을 넣었다.
원본에 없는 값을 임의로 만들어내지 마세요.실제 운영 시스템에서는 이 원칙이 매우 중요하다.
1.17 입력 방향에서도 래퍼를 사용할 수 있다#
지금까지는 레거시 시스템의 출력을 현대적인 데이터로 바꾸었다.
하지만 반대 방향도 가능하다.
이번에는 새로운 서비스가 다음과 같은 데이터를 보낸다고 생각해보자.
{
"username": "홍길동",
"email": "gildong@example.com",
"zip_code": "06234",
"address1": "서울특별시 강남구 테헤란로 124",
"address2": "101동 505호"
}새로운 시스템에서는 매우 자연스러운 구조다.
하지만 레거시 API는 다음과 같은 기괴한 형식을 요구한다.
(06234) 서울특별시 강남구 테헤란로 124|101동 505호이때 래퍼가 중간에서 변환한다.
현대 API
│
▼
구조화된 JSON
│
▼
[래퍼]
│
▼
레거시 형식
│
▼
레거시 API1.18 입력 변환은 AI보다 일반 코드가 먼저다#
여기서 한 가지 중요한 설계 원칙을 기억해야 한다.
다음과 같은 변환은 AI가 필요 없다.
legacy_address = (
f"({zip_code}) "
f"{address1}|{address2}"
)규칙이 완전히 명확하기 때문이다.
이런 작업까지 AI에게 맡기면 오히려 문제가 생긴다.
일반 코드
- 빠름
- 저렴함
- 결정적
- 테스트하기 쉬움
AI
- 비용 발생
- 네트워크 호출 필요
- 지연 시간 발생
- 결과 검증 필요따라서 좋은 AI 래퍼는 모든 변환을 AI에게 맡기는 시스템이 아니다.
다음처럼 설계하는 것이 훨씬 현실적이다.
명확한 규칙
│
▼
일반 코드
복잡한 규칙
│
▼
일반 코드 + 검증
비정형 데이터
│
▼
AI
│
▼
구조화된 출력
│
▼
검증이것이 실전에서의 AI 래핑이다.
1.19 그렇다면 언제 AI를 사용해야 하는가#
다음과 같은 상황에서는 AI가 유용할 수 있다.
다양한 표현을 하나의 의미로 통합해야 하는 경우#
재고있음
재고 보유
주문 가능
구매 가능
판매 가능이런 표현을 하나의 의미로 통합해야 한다면 AI가 유용하다.
{
"in_stock": true
}자연어 데이터에서 정보를 추출해야 하는 경우#
삼성에서 만든 25인치 모니터인데
현재 부산 물류센터에 12대 남아 있습니다.이를 다음과 같이 구조화할 수 있다.
{
"brand": "삼성",
"size": 25,
"location": "부산 물류센터",
"quantity": 12
}서로 다른 시스템의 표현을 통합해야 하는 경우#
Y
YES
가능
사용 가능
활성
ACTIVE이들을 하나의 내부 상태값으로 통합하는 작업도 AI가 도움을 줄 수 있다.
1.20 AI 래퍼의 진짜 장점은 레거시 격리다#
AI 래퍼를 사용하는 가장 중요한 이유가 단순히 "AI가 데이터를 잘 파싱하기 때문"은 아니다.
진짜 장점은 레거시 시스템과 새로운 시스템 사이의 결합도를 낮추는 것이다.
기존 구조에서는 다음과 같다.
신규 서비스
│
├── ^ 구분자 이해
├── | 구분자 이해
├── 가격 변환
├── 재고 상태 변환
├── 예외 처리
└── 레거시 API 규칙 이해
│
▼
레거시 API시간이 지나면 신규 서비스 곳곳에 레거시 시스템의 흔적이 남는다.
반면 래퍼를 사용하면 다음과 같다.
신규 서비스
│
▼
현대적인 API
│
▼
AI 래퍼
│
▼
레거시 API레거시의 복잡성은 래퍼 내부에 가둔다.
이것이 아키텍처적으로 훨씬 중요한 효과다.
1.21 AI 래퍼를 만능 번역기로 생각하면 안 되는 이유#
AI가 있다고 해서 모든 레거시 문제가 해결되는 것은 아니다.
특히 금융, 결제, 재고 수량, 주문 금액처럼 정확성이 중요한 데이터에서는 더욱 조심해야 한다.
예를 들어 다음 데이터가 있다고 하자.
상품가격: 1,200,000원
할인가: 999,000원AI가 가격을 잘못 선택하면 실제 주문 금액에 영향을 줄 수 있다.
따라서 다음과 같은 데이터는 가능하면 일반적인 코드와 데이터베이스 검증을 함께 사용해야 한다.
금액
수량
주문 상태
사용자 ID
권한
결제 상태
재고 수량AI는 의미를 해석할 수 있지만 업무 시스템의 최종 진실 공급원은 아니다.
1.22 지연 시간을 관리하라#
AI API 호출에는 일반적인 함수 호출보다 더 많은 시간이 걸린다.
구조는 다음과 같다.
Client
│
▼
FastAPI
│
▼
Legacy API
│
▼
OpenAI API
│
▼
Structured Output
│
▼
FastAPI
│
▼
Client네트워크 요청이 추가되기 때문에 전체 응답 시간이 증가할 수 있다.
특히 초당 수백~수천 건의 요청이 발생하는 API라면 모든 요청에 LLM을 호출하는 구조는 신중하게 검토해야 한다.
캐시#
같은 원본 데이터가 반복해서 들어온다면 결과를 캐싱할 수 있다.
원본 데이터
│
▼
Hash
│
▼
Cache 확인
/ \
Hit Miss
│ │
▼ ▼
결과 AI 호출
│
▼
Cache비동기 처리#
즉시 결과가 필요하지 않은 작업이라면 비동기 파이프라인으로 분리할 수 있다.
API 요청
│
▼
Queue
│
▼
AI Worker
│
▼
결과 저장이 구조는 대량의 문서 정제나 데이터 마이그레이션에 특히 적합하다.
1.23 비용도 아키텍처의 일부다#
AI API는 호출할 때마다 비용이 발생할 수 있다.
따라서 다음과 같은 구조를 고려해야 한다.
단순 데이터
│
▼
일반 코드
복잡한 데이터
│
▼
저비용 모델
고난도 데이터
│
▼
고성능 모델모든 요청을 가장 비싼 모델로 처리하는 것은 좋은 설계가 아니다.
또한 프롬프트에 불필요한 설명과 대량의 데이터를 반복해서 넣는 것도 비용 증가로 이어질 수 있다.
실제 운영에서는 다음 항목을 모니터링해야 한다.
요청 수
입력 토큰
출력 토큰
평균 응답 시간
오류율
재시도 횟수
캐시 적중률
요청당 비용1.24 민감한 데이터는 더욱 신중하게 다룬다#
레거시 API에는 고객 정보가 포함되어 있을 수도 있다.
예를 들어 다음과 같은 데이터다.
이름
전화번호
주소
이메일
주문정보
계약정보
사내 업무정보이런 데이터를 외부 AI API로 전달한다면 회사의 보안 정책과 데이터 처리 정책을 먼저 검토해야 한다.
특히 AI 래퍼를 만든다는 이유만으로 레거시 데이터 전체를 그대로 모델에 보내서는 안 된다.
가능하면 필요한 데이터만 추출한다.
전체 레거시 응답
│
▼
필요한 필드만 추출
│
▼
AI 처리
│
▼
구조화된 결과OpenAI API의 데이터 보존 및 저장 동작 역시 사용하는 API와 설정에 따라 차이가 있으므로 운영 환경에서는 최신 공식 문서를 확인해야 한다.
1.25 장애가 발생하면 어떻게 할 것인가#
AI API가 항상 성공한다고 가정해서는 안 된다.
다음과 같은 장애가 발생할 수 있다.
레거시 API 장애
AI API 장애
네트워크 장애
타임아웃
스키마 검증 실패
잘못된 원본 데이터
모델 출력 거부
요청 제한따라서 오류를 하나의 유형으로 처리하지 않는 것이 좋다.
API Wrapper
│
┌───────────┼───────────┐
▼ ▼ ▼
Legacy 오류 AI 오류 검증 오류
│ │ │
▼ ▼ ▼
502/504 재시도 격리
│
▼
로그특히 운영 시스템에서는 원본 데이터를 버리지 않는 것이 중요하다.
AI 변환에 실패했을 때 원본 데이터를 확인할 수 있어야 한다.
1.26 재시도에도 규칙이 필요하다#
단순히 다음처럼 작성하면 안 된다.
while True:
try:
call_ai()
break
except:
passAI 서비스가 장애 상태라면 무한 재시도는 장애를 더욱 키울 수 있다.
대신 제한된 재시도 횟수와 백오프를 사용한다.
1차 실패
│
▼
짧은 대기
│
▼
2차 시도
│
▼
실패
│
▼
더 긴 대기
│
▼
3차 시도
│
▼
최종 실패그리고 일정 횟수 이상 실패하면 해당 요청을 별도의 실패 큐로 보내는 방식도 사용할 수 있다.
1.27 AI의 판단과 시스템의 판단을 분리하라#
AI가 다음을 판단했다고 하자.
{
"in_stock": true
}이것을 그대로 주문 가능 여부로 사용해서는 안 된다.
다음과 같이 분리하는 편이 안전하다.
AI
│
▼
"재고 있음으로 해석됨"
│
▼
Pydantic 검증
│
▼
업무 규칙
│
├── 실제 재고 DB 확인
├── 판매 가능 여부 확인
└── 주문 제한 확인
│
▼
최종 판단즉 AI는 해석 계층이고, 최종 비즈니스 규칙은 애플리케이션이 담당해야 한다.
1.28 API 래핑에서 한 단계 더 나아가기#
AI 래퍼는 단순한 데이터 변환에서 끝나지 않는다.
다음과 같은 기능으로 확장할 수 있다.
레거시 API
│
▼
[AI Wrapper]
│
├── 데이터 정제
├── 필드 표준화
├── 분류
├── 추출
├── 요약
├── 이상 데이터 탐지
├── 자연어 변환
└── 구조화
│
▼
현대 API예를 들어 레거시 시스템이 다음과 같은 텍스트를 반환한다고 하자.
상품 P0012 현재 재고 있음.
서울 물류센터에 12개,
부산 물류센터에 5개 보관 중.
최근 입고일은 2026년 9월 20일.새로운 API에서는 다음과 같이 반환하도록 만들 수 있다.
{
"product_id": "P0012",
"available": true,
"warehouses": [
{
"name": "서울 물류센터",
"quantity": 12
},
{
"name": "부산 물류센터",
"quantity": 5
}
],
"last_received_at": "2026-09-20"
}이 순간 래퍼는 단순한 API 변환기가 아니라 레거시 시스템을 위한 의미 변환 계층이 된다.
1.29 AI 래퍼의 올바른 설계 원칙#
실전에서는 다음 원칙을 기억하면 된다.
첫째, AI보다 일반 코드가 먼저다.
명확한 규칙은 코드로 처리한다.
둘째, AI의 출력은 반드시 검증한다.
Pydantic이나 JSON Schema 같은 구조화된 검증 계층을 둔다.
셋째, 원본 데이터를 보존한다.
변환 실패가 발생했을 때 원인을 추적할 수 있어야 한다.
넷째, AI를 비즈니스 규칙의 최종 결정자로 만들지 않는다.
AI는 해석하고 애플리케이션은 결정한다.
다섯째, 비용과 지연 시간을 설계 단계부터 고려한다.
AI 호출은 일반 함수 호출과 다르다.
여섯째, 래퍼를 경계층으로 유지한다.
새로운 서비스가 레거시 시스템의 내부 구조를 다시 알게 된다면 래핑의 목적이 약해진다.
1.30 AI 래퍼와 점진적 현대화#
이 구조가 특히 강력한 이유는 레거시 시스템을 한 번에 교체하지 않아도 되기 때문이다.
처음에는 다음과 같다.
신규 서비스
│
▼
AI Wrapper
│
▼
Legacy API시간이 지나면서 새로운 서비스가 늘어난다.
서비스 A ─┐
서비스 B ─┤
서비스 C ─┼──▶ AI Wrapper ──▶ Legacy API
서비스 D ─┘그리고 언젠가 새로운 재고 시스템을 만들었다고 하자.
서비스 A ─┐
서비스 B ─┤
서비스 C ─┼──▶ API Layer
서비스 D ─┘ │
├──▶ Legacy
│
└──▶ New System마지막에는 레거시 시스템을 제거할 수도 있다.
새로운 서비스
│
▼
현대적인 API
│
▼
새로운 시스템즉 AI 래퍼는 레거시 시스템을 영원히 유지하기 위한 장치가 아니라 점진적인 현대화를 위한 완충 계층으로 활용할 수 있다.
1.31 데이터 청소부에서 시스템 설계자로#
이 장의 시작에서 박 대리는 레거시 데이터의 문제를 해결하기 위해 하루 종일 split()과 if를 작성하고 있었다.
하지만 문제의 본질은 코드가 부족해서가 아니었다.
문제는 서로 다른 세대의 시스템이 서로 다른 언어를 사용하고 있다는 것이었다.
레거시 시스템은 다음과 같은 언어를 사용한다.
^
|
/
"가격문의"
"재고보유"새로운 시스템은 다음과 같은 언어를 원한다.
{
"product_id": "...",
"price": 25000,
"in_stock": true
}그 사이에 AI 기반 래퍼를 놓으면 두 시스템 사이의 경계를 명확하게 만들 수 있다.
┌──────────────────┐
│ 신규 서비스 │
│ │
│ 깨끗한 JSON │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ AI API Wrapper │
│ │
│ 의미 해석 │
│ 구조화 │
│ 검증 │
│ 오류 처리 │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 레거시 시스템 │
│ │
│ 오래된 API │
│ 비정형 데이터 │
└──────────────────┘이것이 AI 시대의 API 래핑이 갖는 의미다.
AI를 시스템 전체에 무작정 집어넣는 것이 아니다.
AI가 잘하는 일과 일반 코드가 잘하는 일을 분리하고, 그 사이에 명확한 경계를 만드는 것.
그렇게 하면 오래된 시스템을 당장 버리지 않고도 새로운 서비스와 연결할 수 있다.
그리고 더 중요한 것은 이 과정에서 레거시 시스템의 복잡성이 새로운 시스템 전체로 퍼지는 것을 막을 수 있다는 점이다.
1.32 마무리#
API 래핑은 새로운 기술이 아니다.
하지만 LLM과 구조화된 출력을 결합하면서 그 역할은 훨씬 넓어지고 있다.
과거의 래퍼가 주로 다음을 처리했다면,
XML → JSON
SOAP → REST
인증 A → 인증 BAI 기반 래퍼는 여기에 다음과 같은 역할을 추가할 수 있다.
비정형 텍스트
↓
의미 해석
↓
정보 추출
↓
구조화
↓
스키마 검증
↓
현대적인 API다만 AI가 모든 것을 대신하는 것은 아니다.
명확한 규칙은 코드가 처리하고, 의미 해석이 필요한 부분은 AI가 담당하며, AI의 결과는 스키마와 업무 규칙으로 검증하는 것이 핵심이다.
결국 좋은 AI 래퍼는 AI를 많이 사용하는 시스템이 아니라 AI를 필요한 곳에 정확하게 사용하는 시스템이다.
레거시 시스템을 없애지 않고도 새로운 서비스를 만들 수 있다.
오래된 API를 그대로 둔 채 새로운 데이터 계약을 제공할 수 있다.
그리고 복잡한 시스템을 한 번에 뜯어고치는 대신 작은 경계층부터 시작해 점진적으로 현대화할 수 있다.
박 대리에게 필요한 것은 레거시 시스템을 당장 없애는 것이 아니었다.
필요했던 것은 그 복잡한 세계와 새로운 서비스 사이에 하나의 제대로 된 경계를 만드는 것이었다.
그 경계가 바로 AI 기반 API 래퍼다.
#관련 참고 도서#
레거시 스파게티코드 AI로 심폐소생술#
10년 넘게 쌓인 레거시 코드와 기술 부채를 무작정 재구축하지 않고, AI를 활용해 기존 시스템을 유지하면서 점진적으로 현대화하는 방법을 다루는 실전 가이드입니다.
레거시 코드 분석부터 데이터 구조화, 자연어 기반 데이터 접근, 로컬 AI, AI 에이전트를 활용한 운영 자동화까지 기존 시스템에 AI를 접목하는 다양한 방법을 살펴봅니다.