2332 words
12 minutes
httpx2, Pydantic이 이어받은 httpx — 바꿔도 될까, 얼마나 빨라졌을까

FastAPI 프로젝트에서 테스트를 돌리다가 낯선 warning을 만났습니다.

StarletteDeprecationWarning: Using `httpx` with `starlette.testclient`
is deprecated; install `httpx2` instead.

httpx 2.0이 나온 건가 싶었는데 아니더라고요. httpx2는 Pydantic 팀이 httpx를 통째로 포크한 별도 패키지였습니다. Starlette 1.2+의 TestClient가 이미 httpx2 기반으로 넘어가면서 기존 httpx 경로를 deprecated 처리한 상황이었습니다. 이참에 httpx2가 무엇인지, 기존 httpx와 무엇이 다른지, 그대로 바꿔도 되는지를 제대로 조사했고 이 글에 그 결과를 정리합니다.

httpx2가 나온 배경#

httpx2는 Pydantic 팀이 2026년 5월에 시작한 encode/httpx의 공식 후속 포크입니다. 원본 httpx는 requests의 뒤를 잇는 표준 HTTP 클라이언트로 자리 잡았지만, 정작 유지보수 활동은 한동안 뜸했습니다. 그 공백을 Pydantic이 “신뢰할 수 있는 유지보수 경로와 신속한 보안 업데이트”를 내걸고 이어받은 것입니다.

첫 릴리스 v2.0.0이 2026년 5월 12일에 나왔고, 두 달 만에 v2.7.0까지 왔습니다. 상당히 빠른 속도입니다. 커뮤니티 반응도 흥미로운데, Pydantic보다 먼저 httpxyz라는 별도 포크를 만들었던 그룹조차 “httpx2가 ‘blessed’ 포크가 되어야 한다”며 자기 포크 대신 httpx2를 지지하고 나섰습니다. 사실상 생태계의 합의가 끝난 셈이죠.

httpx 대비 달라진 점#

API 자체는 포크 시점의 최신 httpx와 완전 호환이고, 그 위에 개선이 쌓였습니다.

  • 인증서: certifi 대신 truststore를 사용해 OS 신뢰 저장소를 그대로 따라갑니다
  • 압축: Python 3.14+에서 stdlib compression.zstd로 zstd를 기본 활성화했습니다
  • 구조: 별도 패키지였던 httpcore를 저장소에 병합해 의존성이 단순해졌습니다
  • 성능: memoryview 기반 버퍼 처리, anyio fast_acquire 최적화 (v2.3.0)
  • 동시성: RLock 기반 스레드 안전성 개선, HTTP/2 semaphore 버그 수정 (v2.4.0)
  • 보안: 연쇄 Content-Encoding 디코더를 5개로 제한해 압축 폭탄을 방어합니다 (v2.4.0)
  • 신기능: 네이티브 WebSocket 지원(httpx-ws 벤더링), QUERY 메서드 지원 (v2.6.0)

이 중에서 눈여겨볼 것이 anyio fast_acquire입니다. 원래 encode/httpcore에 PR로 올라와 있었는데 방치되던 최적화를 httpx2가 흡수한 것이거든요. 유지보수가 살아있는 포크의 장점이 바로 이런 데서 드러납니다.

직접 작성한 코드는 사실상 drop-in#

직접 작성한 코드에서는 사실상 drop-in입니다. 패키지 이름과 모듈 이름이 httpx2로 바뀌었으니 import만 고치면 됩니다.

# before
import httpx
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app), base_url="http://testserver"
) as client:
...
# after
import httpx2
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://testserver"
) as client:
...

실제로 :%s/httpx/httpx2 치환만으로 업그레이드를 끝냈다는 보고가 있을 정도입니다. 저도 FastAPI 프로젝트의 테스트 코드를 이 방식으로 옮겼고, 185개 테스트가 전부 그대로 통과했습니다.

다만 함정이 하나 있습니다. httpx 객체를 주고받는 서드파티 라이브러리는 여전히 httpx 타입에 바인딩되어 있다는 점입니다. 제 프로젝트에서 딱 그런 코드가 나왔습니다.

# OpenAI SDK 호출을 감싼 어댑터 코드
except httpx.TimeoutException as exc:
...

이 예외는 OpenAI SDK가 내부에서 던지는 것이라서, httpx2.TimeoutException으로 바꾸면 오히려 예외를 못 잡게 됩니다. 이름만 같을 뿐 다른 클래스거든요. opentelemetry-instrumentation-httpx처럼 httpx 자체를 계측하는 라이브러리도 마찬가지입니다. 이런 코드는 해당 SDK가 httpx2로 넘어갈 때까지 httpx로 남겨둬야 합니다. 다행히 두 패키지는 모듈 이름이 달라서 충돌 없이 공존할 수 있고, 당분간 둘 다 설치되어 있는 것이 정상적인 상태입니다.

벤치마크로 본 속도 차이#

마침 httpx vs httpx2 vs aiohttp를 같은 조건에서 돌린 벤치마크가 있습니다 (Python 3.14.6, httpx 0.28.1 / httpx2 2.4.0 / aiohttp 3.14.1, 로컬 서버). 코드까지 직접 확인해봤는데 AsyncClient + asyncio.gather() 기반, 즉 전부 async 기준입니다.

시나리오 (mean)httpxhttpx2aiohttp
직렬 1000건, 풀링 없음1.087ms1.066ms0.279ms
직렬 1000건, 풀링 있음0.540ms0.487ms0.091ms
병렬 100건×10배치, 풀링 있음526.5ms92.1ms5.2ms

표에서 두 가지가 드러납니다. 직렬 요청에서는 httpx와 httpx2가 거의 같고, 병렬 + 커넥션 풀링 조합에서는 격차가 폭발합니다. httpx2가 httpx보다 5.7배 빠릅니다.

이 마지막 줄이 핵심입니다. 원본 httpx는 커넥션 풀링을 켜고 동시 요청을 쏘면 오히려 성능이 붕괴하는 병목이 있었습니다. median이 704ms까지 치솟아서 풀링을 끈 것보다 6배 이상 느려지는, 납득하기 어려운 상황이었죠. encode 저장소에 “not production ready under load”라는 제목으로 보고됐던 바로 그 문제입니다. httpx2는 락 경합의 원인을 anyio fast_acquire와 RLock 전환으로 고쳐서 이 병목을 제거했습니다.

sync 클라이언트는 별도 벤치마크가 없지만 사실상 httpx와 동일하다고 보면 됩니다. fast_acquire는 async 경로 전용 개선이고, sync에 해당하는 변경은 memoryview(소폭)와 RLock(속도가 아닌 멀티스레드 안전성) 정도이기 때문입니다.

aiohttp가 더 빠른데도 생태계 기본값은 httpx인 이유#

표를 다시 보면 aiohttp가 httpx2보다도 직렬 5배, 병렬 18배 빠릅니다. httpx2가 고친 것은 httpx의 병적인 풀링 병목이지, aiohttp를 따라잡은 것이 아닙니다. 격차의 원인은 구조적입니다. aiohttp는 asyncio 전용으로 설계된 단일 목적 클라이언트에 C 확장 가속까지 얹었고, httpx 계열은 sync/async 겸용 추상화 레이어(transport 계층, anyio, 순수 파이썬 sans-IO 파서)를 거치기 때문입니다.

그런데도 생태계의 기본값은 httpx입니다. 가장 큰 이유는 requests와 같은 API를 sync/async 양쪽에서 제공한다는 점입니다. aiohttp는 async 전용에 API 스타일도 완전히 다릅니다. requests를 알면 바로 쓸 수 있고, async가 필요해져도 같은 라이브러리를 쓴다는 것이 가장 큰 진입 이유입니다.

SDK 저자에게 이상적인 구조라는 점도 큽니다. OpenAI, Anthropic SDK가 httpx를 쓰는 이유이기도 한데, sync·async 클라이언트를 하나의 코드베이스로 만들 수 있고 transport 추상화로 HTTP 레이어를 갈아끼울 수 있습니다. openai[aiohttp]가 정확히 그 패턴입니다. API는 httpx 스타일을 유지하면서, 성능이 필요한 밑단만 aiohttp로 교체하는 것이죠.

테스트 생태계의 표준이라는 점도 무시할 수 없습니다. ASGITransport로 FastAPI/Starlette 앱을 네트워크 없이 인프로세스로 호출할 수 있습니다. Starlette TestClient가 httpx2 기반인 것도, respx 같은 mocking 도구가 붙어 있는 것도 이 때문입니다. 여기에 기본값의 안전함도 더해집니다. 기본 timeout이 있고(requests·aiohttp식 무한 대기 없음), redirect를 명시적으로 처리하며, 전체 타입 힌트와 HTTP/2 지원, trio 호환까지 갖추고 있습니다.

그리고 결정적으로, 실제 API 호출은 네트워크 레이턴시가 지배합니다. 수십에서 수백 ms짜리 왕복에서 클라이언트 오버헤드 0.4ms vs 0.09ms는 측정조차 어렵습니다. 위 벤치마크는 로컬호스트 마이크로벤치마크라 격차가 극대화된 값입니다. aiohttp의 우위가 실질적인 것은 대량 크롤링이나 fan-out처럼 초당 수백에서 수천 건 요청을 쏟아내는 워크로드뿐입니다.

그래서 이렇게 정리했습니다#

제 프로젝트의 선택 기준은 다음과 같이 정리됐습니다.

  • 기본값은 httpx2 — 테스트, 저빈도 API 호출, SDK 연동. DX와 생태계가 이유입니다
  • 실측된 hot path만 aiohttp — 고빈도 아웃바운드 호출은 aiohttp 클라이언트와 openai[aiohttp] 유지
  • 서드파티가 끌어오는 httpx는 억지로 걷어내지 않기 — SDK들이 httpx2로 넘어갈 때 따라가면 됩니다

테스트 코드의 httpx를 httpx2로 옮기는 데 든 작업은 import 두 줄 수정이 전부였고, 테스트는 전부 통과했습니다. Starlette가 이미 방향을 정했고 Pydantic이 유지보수를 책임지는 이상, 새 프로젝트라면 처음부터 httpx2로 시작하는 것이 맞다고 봅니다.

참고한 자료는 다음과 같습니다.

httpx2, Pydantic이 이어받은 httpx — 바꿔도 될까, 얼마나 빨라졌을까
https://monologg.kr/posts/2026/07/17/2026-07-17-httpx2-pydantic-fork-migration-benchmark/
Author
Jangwon Park
Published at
2026-07-17
License
CC BY-NC-SA 4.0