View

출처: https
OpenAI API를 처음 접했을 때 상당히 의아했다. curl 예제를 복사해 붙여 넣는데 전부 POST /v1/chat/completions 하나로 처리된다. GET으로 대화 목록을 가져오거나, PUT으로 수정하거나, DELETE로 삭제하는 패턴이 없다. Anthropic도 동일하다. POST /v1/messages 하나로 끝난다. Gemini 또한 마찬가지다.
REST 원칙을 철저히 학습한 백엔드 개발자 입장에서는 다소 당혹스럽다. "이들은 REST를 모르는가? 아니면 알면서도 지키지 않는 것인가?"라는 의문이 든다. 다만 조금만 파고들면 몰라서 그런 것이 아님을 알 수 있다. LLM이라는 도메인 자체가 REST 프레임에 맞지 않기 때문에 그렇게 설계한 것이다. 이 글에서는 LLM API와 REST의 차이를 설계 철학부터 실제 엔드포인트, 자사 래퍼 API를 만들 때 참고해야 할 사항까지 전부 정리한다.
RESTful API 패턴 간단히 복습
LLM API 이야기를 하기 전에 REST가 무엇이었는지 빠르게 정리하고 넘어간다. 이 부분을 알아야 왜 LLM API가 이상하게 보이는지 체감할 수 있다.
HTTP 동사와 CRUD 매핑
REST는 2000년에 Roy Fielding 박사가 박사 논문에서 정리한 아키텍처 스타일이다. 한 줄로 요약하면 세상 모든 것을 리소스로 보는 관점이다. 리소스 하나에 URI 하나를 붙이고, HTTP 동사로 조작한다. GET은 조회, POST는 생성, PUT은 전체 교체, PATCH는 부분 수정, DELETE는 삭제다. 이렇게 다섯 개의 동사가 CRUD에 거의 1:1로 대응된다.
멱등성, 상태 없음, 리소스 URI
REST에는 중요한 원칙 몇 가지가 있다. 우선 멱등성(idempotency)이다. GET, PUT, DELETE는 몇 번을 호출해도 결과가 같아야 한다. 같은 글을 10번 PUT해도 상태는 동일하다. POST만 비멱등이다. 호출할 때마다 새 리소스가 생성되기 때문이다.
그리고 상태 없음(stateless)이다. 서버는 클라이언트 상태를 기억하지 않는다. 매 요청이 완결되어야 한다. 따라서 세션 관리는 토큰으로 위임하고 서버는 리소스만 다룬다.
블로그 글 CRUD API 예시
전형적인 REST API는 다음과 같은 형태다.
GET /api/posts # 글 목록
GET /api/posts/123 # 123번 글 조회
POST /api/posts # 새 글 작성
PUT /api/posts/123 # 123번 글 수정
DELETE /api/posts/123 # 123번 글 삭제
깔끔하다. URL만 봐도 무엇을 하는지 감이 온다. 프론트엔드 개발자에게 문서를 주지 않아도 대략적인 유추가 가능하다. 이것이 REST의 매력이다.
LLM API는 왜 전부 POST만 사용하는가

출처: xenoss.io
각설하고, 이제 LLM API를 살펴보자. REST가 머릿속에 가득 찬 상태에서 OpenAI 문서를 열면 무언가 허전하다. GET, PUT, DELETE가 잘 보이지 않는다. 전부 POST다.
/v1/chat/completions, /v1/messages 엔드포인트 해부
OpenAI의 메인 엔드포인트는 POST /v1/chat/completions 하나다. Anthropic은 POST /v1/messages 하나, Google Gemini는 POST /v1beta/models/{model}:generateContent이다. 세 회사 모두 메인 호출은 POST 하나로 집중되어 있다.
호출 예시는 다음과 같다.
curl https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [
{"role": "user", "content": "안녕"}
]
}'
여기서 이상한 점 하나가 발견된다. URL이 completions이다. 명사 같기도 하고 동사 같기도 하다. 엄밀히 말하면 이는 리소스가 아니라 "완성을 만들어 달라"는 동작(action)이다.
프롬프트는 리소스가 아니라 동작이다
REST 원리주의로 보면 /completions는 URL로 사용해서는 안 된다. 이는 리소스 명사가 아니라 동사이기 때문이다. 그렇다면 왜 이렇게 만들었을까? 답은 간단하다. 프롬프트는 저장되는 리소스가 아니고, 매번 생성되는 응답이 메인이기 때문이다.
블로그 글 API에서 POST /posts는 "새 글을 DB에 저장한다"는 의미다. 반면 POST /chat/completions는 "프롬프트를 받아 즉석에서 모델을 돌려 응답을 생성한다"는 의미다. 저장도 되지 않는다(일반적으로는). 따라서 개념이 CRUD가 아니라 함수 호출에 가깝다. RPC(Remote Procedure Call) 스타일인 것이다.
멱등성 포기한 이유: 같은 프롬프트도 매번 다른 결과
REST 원칙 중 하나가 멱등성이지만, LLM은 원천적으로 멱등할 수 없다. temperature=0.7이면 같은 프롬프트를 10번 넣어도 10개 모두 다른 응답이 나온다. temperature=0으로 설정해도 모델 버전이 업데이트되면 또 달라진다.
그래서 GET으로 만들 수가 없다. GET은 캐시되고 멱등해야 하는데, LLM은 두 조건 모두 맞지 않는다. 억지로 GET으로 만들면 프록시나 CDN이 캐싱해서 혼란이 발생한다. POST가 정답이다.
OpenAI, Anthropic, Google Gemini 엔드포인트
세 회사의 메인 엔드포인트를 묶어서 보면 패턴이 한눈에 들어온다. OpenAI는 /v1/chat/completions, /v1/embeddings, /v1/images/generations 전부 POST다. Anthropic은 /v1/messages, /v1/messages/count_tokens 전부 POST다. Google Gemini는 아예 URL에 콜론(:)으로 동사를 붙여 models/{model}:generateContent와 같이 노골적으로 RPC 스타일로 설계했다. 구글도 REST를 모르는 것이 아니라, 이 도메인에서는 RPC가 더 낫다고 판단한 것이다.
REST vs RPC vs GraphQL vs LLM API 4개 비교

경쟁 글들은 보통 REST vs RPC만 비교하고 끝낸다. 다만 2026년 시점에서는 GraphQL과 LLM API까지 4개를 함께 봐야 그림이 완성된다.
4개 패러다임 한눈에 보기
| 항목 | REST | RPC (gRPC 등) | GraphQL | LLM API |
| 기본 단위 | 리소스 | 프로시저(함수) | 스키마 기반 쿼리 | 프롬프트 → 응답 |
| 주요 동사 | GET/POST/PUT/DELETE | 대부분 POST | POST 하나 | POST 하나 |
| 엔드포인트 | 많음 (리소스마다) | 함수마다 | 하나 (`/graphql`) | 몇 개 (`/chat`, `/embeddings`) |
| 응답 형태 | 고정된 JSON | 고정된 proto | 쿼리에 맞춘 JSON | 비결정적 텍스트/JSON |
| 멱등성 | GET/PUT/DELETE는 멱등 | 함수에 따라 다름 | 쿼리는 멱등, 뮤테이션 아님 | 기본적으로 비멱등 |
| 스트리밍 | 기본적으로 X | gRPC는 지원 | subscription 있음 | SSE 기본 |
LLM API는 RPC에 가깝지만 완전한 RPC는 아니다
위 표만 봐도 LLM API는 RPC와 가장 유사하다. 함수 하나를 호출하여 결과를 받는다는 점이 같다. 그렇다고 순수 RPC도 아니다. 우선 스트리밍이 기본이어야 한다는 점이 다르다. 일반 RPC는 요청-응답 한 사이클로 끝나지만 LLM은 토큰을 한 글자씩 흘려보낸다. 입력 크기도 차원이 다르다. gRPC의 proto 메시지는 보통 수 KB 정도이지만 LLM은 컨텍스트 윈도우를 꽉 채우면 수백 KB를 전송한다. 응답이 비결정적이라는 점도 RPC와 다르다. RPC 함수는 같은 입력이면 같은 출력을 내지만 LLM은 그렇지 않다.
종합하면 LLM API는 RPC 기반에 스트리밍과 비결정성을 얹은 변종이라고 봐야 한다.
GraphQL은 왜 아닌가? 구조화된 응답이 어렵기 때문이다
GraphQL도 POST 하나로 모두 처리한다는 점은 비슷하다. 다만 GraphQL은 클라이언트가 원하는 필드를 쿼리하면 서버가 정확히 그것만 돌려주는 것이 강점인데, LLM은 이것이 불가능하다.
"이름과 나이만 달라"고 해도 모델이 "안녕하세요 제 이름은 철수이고 나이는 스물다섯이며..."와 같은 식으로 말을 시작한다. JSON 모드나 structured output을 붙여서 어느 정도 맞출 수는 있지만, GraphQL의 정적 스키마 보장 수준에는 미치지 못한다. 도메인 자체가 맞지 않는 것이다.
LLM API 설계할 때 고려되는 특수 조건들
왜 LLM API가 REST 원칙을 포기했는지 더 깊이 파보면 이 도메인만의 특수한 사정이 있다.
스트리밍(SSE) 응답, REST 프레임에 맞지 않는다
LLM이 긴 응답을 만들 때 전체가 완성될 때까지 기다리면 UX가 망가진다. 그래서 Server-Sent Events(SSE)로 토큰을 한 개씩 흘려보낸다. OpenAI든 Anthropic이든 stream: true 옵션을 주면 SSE 응답이 된다.
문제는 REST가 기본적으로 요청-응답 1사이클을 가정한다는 점이다. 스트리밍은 본래 REST 프레임에 끼워 맞춰지지 않는다. 그래서 HTTP 커넥션을 오래 유지하고 text/event-stream content-type으로 청크 단위로 흘리는 방식으로 우회한다. 이는 REST-like하지 않지만, 실용상 이 방식이 맞다.
대용량 프롬프트 전송, URL 쿼리에 담을 수 없다
REST에서 GET 요청은 쿼리 파라미터로 데이터를 전달한다. 다만 URL 길이는 브라우저마다 2~8KB 정도가 한계다. LLM 프롬프트는 쉽게 100KB를 넘어간다. 컨텍스트 윈도우가 128k, 200k 토큰인 시대이기 때문이다.
GET으로는 불가능하다. POST 본문에 JSON으로 담아야 한다. 그래서 조회성 호출이 되어야 할 것도 POST로 처리할 수밖에 없다.
토큰, 비용, 레이트 리밋, 상태성이 부분적으로 필요하다
REST는 stateless가 원칙이지만, LLM API는 완전한 stateless가 되지 못한다. 토큰 사용량은 월 쿼터 때문에 서버가 누적 상태를 알아야 하고, RPM(분당 요청)이나 TPM(분당 토큰) 같은 레이트 리밋도 서버가 기억해야 한다. Anthropic의 prompt caching처럼 이전 호출의 프리픽스를 서버가 기억하는 기능도 존재한다. 표면적으로는 stateless지만 내부는 상태성이 상당히 있다. 이것이 완전한 REST가 아닌 또 하나의 이유다.
Function calling, Tool use, MCP, 계속 진화 중
최근 몇 년간 LLM API는 도구 호출(tool use) 기능이 필수가 되었다. 모델이 "이 함수를 실행해 달라"고 응답하면 클라이언트가 실행하고 결과를 다시 모델에게 돌려주는 구조다. 한 번의 요청-응답으로 끝나지 않고 여러 번 왕복한다.
2025년 이후에는 MCP(Model Context Protocol)가 자리 잡으면서 LLM이 외부 도구와 표준 프로토콜로 통신하게 되었다. 이제 REST고 뭐고 할 것 없이 LLM-native한 새 표준이 만들어지는 중이다.
자사 LLM 래퍼 API 설계 시 무엇을 따라야 하는가

출처: DEV Community
여기서부터가 실무 이야기다. 내부에서 LLM 래퍼 서비스를 만든다면 REST처럼 만들 것인가, LLM API 스타일로 만들 것인가?
리소스 중심 vs 액션 중심 케이스별 선택법
대화 이력을 DB에 저장하고 관리하는 것이 메인이면 REST가 낫다. 대화(conversation)가 리소스이므로 GET /conversations/123과 같은 식으로 만들 수 있다. 반면 프롬프트를 받아 즉석에서 응답만 생성하고 저장하지 않는다면 OpenAI 스타일 RPC가 낫다. POST /generate 하나로 끝난다.
둘 다 해야 한다면 하이브리드로 가도 된다. 대화는 REST, 생성은 RPC다. 딱 잘라서 하나만 고를 필요는 없다. 기능별로 섞어 써도 된다.
하이브리드 설계 사례 (Anthropic Messages API 스타일)
Anthropic Messages API는 꽤 깔끔한 설계다. 메인은 POST /v1/messages 하나지만, POST /v1/messages/count_tokens 같은 보조 유틸 엔드포인트를 별도로 만들어 두었다. 부가 기능은 별도 경로로 분리하고, 메인 호출만 단일 POST로 유지하는 패턴이다.
내부 래퍼를 만들 때도 이 패턴을 추천한다.
POST /v1/chat # 메인 생성
POST /v1/chat/stream # 스트리밍 버전 (optional)
POST /v1/chat/count_tokens # 토큰 수 체크
GET /v1/conversations # 저장된 대화 목록 (저장한다면)
GET /v1/conversations/:id # 특정 대화 조회
메인은 RPC, 부가는 REST다. 두 패러다임의 장점을 섞어 쓰는 것이다.
버전 관리, 에러 응답, Idempotency-Key 활용
버전 관리는 /v1/, /v2/ 경로로 분리하는 것이 무난하다. 프롬프트 스키마가 바뀔 수 있는데, 헤더 기반 버전은 디버깅이 어렵기 때문에 경로 방식을 추천한다.
에러 응답은 HTTP 상태 코드(4xx/5xx)에 본문 {"error": {"type": "...", "message": "..."}} 구조를 얹는 식이다. OpenAI 스타일을 따르면 무난하다.
Idempotency-Key 헤더도 챙겨 두면 좋다. 같은 POST가 두 번 찍혀도 중복 과금이 되지 않도록 클라이언트가 UUID를 생성해 헤더로 보내는 방식이다. Stripe가 시작한 패턴인데 LLM API에도 유용하다.
앞으로 LLM API는 어떻게 바뀔 것인가
Agentic API 트렌드 (MCP, Computer Use)
2025년 후반부터 agentic API가 부상하고 있다. LLM이 단순히 텍스트를 생성하는 것이 아니라, 도구를 호출하고 파일을 읽고 브라우저를 조작하고 화면을 보면서 클릭까지 한다. Anthropic의 Computer Use, OpenAI의 Operator 같은 기능들이다.
이렇게 되면 API는 더 이상 "프롬프트를 보내고 답을 받는" 것이 아니다. "에이전트 세션을 열고, 여러 번 왕복하면서, 도구 호출 결과를 계속 주고받는" 방식이 된다. 이는 REST도 아니고 RPC도 아니며, 거의 WebSocket 같은 지속 연결 프로토콜이 필요하다.
스트리밍이 기본이 되는 시대
지금도 스트리밍이 거의 필수지만, 앞으로는 아예 non-streaming 호출이 deprecated 될 수도 있다. 특히 긴 reasoning을 수행하는 모델들(OpenAI o1 계열, Anthropic extended thinking)은 응답 대기 시간이 분 단위다. 스트리밍 없이는 UX가 나오지 않는다.
SSE가 계속 쓰이겠지만, WebTransport나 gRPC streaming 같은 더 효율적인 프로토콜로 옮겨갈 가능성도 있다.
LLM-native 패턴으로 수렴할 전망
REST는 웹 문서 도메인에 최적화된 모델이었다. LLM은 다른 도메인이므로 다른 패턴이 나오는 것이 당연하다. 앞으로 몇 년 뒤에는 "LLM API는 원래 이렇게 만드는 것이다"라는 새 표준이 자리 잡을 것이다. MCP가 그 후보 중 하나이고, OpenAI Responses API 같은 움직임도 그 방향이다.
REST 원리주의에 갇혀 "이들은 왜 이렇게 만들었는가" 불평할 필요는 없다. 도메인이 다르면 설계도 달라져야 한다.
LLM API가 POST만 쓰는 진짜 이유는 무엇인가?
정리하면 LLM API와 REST의 차이는 이렇게 요약된다. REST는 리소스 중심이고 LLM API는 동작 중심이다. 프롬프트는 저장되는 리소스가 아니라 매번 실행되는 함수 호출에 가깝다. POST만 사용하는 것이 정상이다. 멱등성이 없고, 입력 크기가 크며, 스트리밍이 기본이고, 비결정적 응답이기 때문에 GET/PUT/DELETE에 맞지 않는다.
LLM API는 RPC에 가장 가깝지만 스트리밍과 상태성이 섞인 변종이다. 자사 래퍼를 만들 때는 하이브리드 설계를 추천한다. 생성은 RPC 스타일, 저장/조회는 REST 스타일로 섞는 방식이다. 앞으로는 agentic + 스트리밍 중심의 LLM-native 패턴으로 수렴할 것이다. MCP 같은 표준을 주목해야 한다.
REST를 지키지 않는다고 게으른 설계는 아니다. 도메인이 다르므로 다르게 만든 것이다. 공식 문서를 읽으면서 "왜 이렇게 했을까" 고민하지 말고, "이 도메인에 맞춰 이렇게 한 것이구나"라고 받아들이면 머리가 편해진다. 내부 API를 만들 때도 REST/RPC 둘 중 하나만 고집하지 말고, 기능에 맞게 섞어 쓰는 것이 실용적이다.
나 역시 회사에서 LLM 래퍼를 작성하다 처음에 REST로 빡빡하게 잡았다가 스트리밍을 붙이는 단계에서 반쯤 갈아엎은 경험이 있다. 다음 글에서는 그때 FastAPI로 어떻게 다시 설계했는지 코드까지 풀어볼 예정이다.
'Backend' 카테고리의 다른 글
| 라라벨(Laravel)이 PHP 프레임워크 중 점유율 1위인 이유를 수치로 정리한다 (0) | 2026.04.26 |
|---|---|
| FastAPI는 누가 만들었고, 왜 빠른가. 비동기면 무조건 빠르다고 알고 있는가? (0) | 2026.04.23 |
| Good Retry, Bad Retry 장애 스토리 (0) | 2024.11.27 |
| FastAPI의 Uvicorn + Gunicorn 결합은 반드시 필요한 것인가? (0) | 2024.04.10 |
| FastAPI 동기/비동기 동작 방식 분해해보기 (0) | 2024.04.05 |
