서버리스 어댑터의 작은 버그는 왜 입력 계약을 다시 보게 만들까
Hono 같은 얇은 웹 프레임워크를 AWS Lambda, Bun, 정적 파일, validator와 함께 쓸 때 “요청이 들어왔다”는 사실만 믿어도 될까?
서버리스 어댑터의 작은 버그는 왜 입력 계약을 다시 보게 만들까
- 카테고리: web_app_dev
- 예상 읽기 시간: 10분
- 오늘의 질문: Hono 같은 얇은 웹 프레임워크를 AWS Lambda, Bun, 정적 파일, validator와 함께 쓸 때 “요청이 들어왔다”는 사실만 믿어도 될까?
- 핵심 출처:
- honojs/hono v4.12.28 - 2026-07-06, 확인일 2026-07-08
- Validation - Hono - n.d., 확인일 2026-07-08
- AWS Lambda - Hono - n.d., 확인일 2026-07-08
- Create AWS Lambda proxy integrations for HTTP APIs in API Gateway - n.d., 확인일 2026-07-08
- Content-Type header - MDN Web Docs - n.d., 확인일 2026-07-08
1. 왜 지금 봐야 하나
2026년 7월 6일 Hono v4.12.28 릴리스는 큰 기능 발표처럼 보이지 않는다. 변경 목록도 작다. serve-static에서 빈 문자열 콘텐츠를 found로 취급하고, Content-Type media type을 case-insensitive로 정규화하고, Bun WebSocket의 requested subprotocol을 WSContext.protocol에 보고하고, AWS Lambda adapter가 V2 event를 rawPath만이 아니라 requestContext로 감지하도록 고쳤다.
그런데 웹앱 개발자에게는 이런 “작은 수정”이 오히려 공부할 가치가 있다. 실제 서비스 장애는 대개 거창한 아키텍처 용어보다 이런 경계에서 난다.
- 클라이언트가
Application/JSON처럼 대소문자가 섞인Content-Type을 보냈을 때 validator가 본문을 비워 버리는가? - 정적 파일이 존재하지만 내용이 빈 문자열이면 404로 처리되는가, 200으로 처리되는가?
- API Gateway HTTP API payload format 2.0 이벤트와 Lambda Function URL 이벤트를 같은 모양으로 오해하는가?
- WebSocket handshake에서 클라이언트가 요청한 subprotocol과 서버가 선택한 protocol을 정확히 구분하는가?
오늘의 핵심 문장: 서버리스 웹앱에서 어댑터는 단순 연결 코드가 아니라 HTTP, 런타임, 클라우드 이벤트를 내 앱의 입력 계약으로 번역하는 컴파일러다.
2. 핵심 개념
Hono 같은 웹 표준 기반 프레임워크는 Request와 Response를 중심으로 코드를 단순하게 만든다. 하지만 실제 배포 환경은 순수한 Request만 주지 않는다.
AWS API Gateway는 Lambda에 JSON event를 보낸다. AWS 공식 문서에 따르면 HTTP API의 payload format은 1.0과 2.0이 있고, 2.0에는 rawPath, cookies, requestContext.http, lowercased headers 같은 필드가 들어간다. 반대로 1.0에는 multiValueHeaders, multiValueQueryStringParameters, path 같은 필드가 있다. Hono의 hono/aws-lambda adapter는 이런 event를 웹 표준 요청처럼 다룰 수 있게 바꾼다.
여기서 중요한 점은 “거의 같은 모양”이 “같은 계약”은 아니라는 것이다. Hono v4.12.28의 AWS Lambda 수정은 V2 이벤트를 rawPath 존재 여부만으로 판단하지 않고 request context를 보도록 바꿨다. rawPath라는 필드 하나만 보고 런타임 이벤트를 분류하면, 비슷한 필드를 가진 다른 이벤트나 edge case에서 잘못된 변환이 일어날 수 있다.
Content-Type도 마찬가지다. Hono validation 문서는 json이나 form을 검증하려면 요청에 맞는 content-type 헤더가 있어야 하며, 없으면 body가 파싱되지 않아 {}가 들어온다고 설명한다. MDN은 Content-Type이 요청 본문의 media type을 서버에 알려 주는 헤더이고, 엄격한 서버는 지원하지 않는 타입에 415 Unsupported Media Type을 줄 수 있다고 설명한다. 그러면 validator 입장에서는 application/json인지 판별하는 일이 곧 입력 계약이다.
문제는 HTTP 세계의 값들이 항상 우리가 테스트에서 쓴 문자열 그대로 오지 않는다는 점이다. media type과 parameter, 헤더 이름 대소문자, boundary, charset, 프록시가 합친 중복 헤더가 모두 섞인다. 그래서 v4.12.28의 “Content-Type media type을 case-insensitive matching으로 정규화”는 작은 편의가 아니라, 현실의 HTTP 입력을 스펙에 맞게 받아들이는 수정이다.
3. 최신 이슈와 연결
이번 Hono 릴리스를 세 묶음으로 보면 웹앱의 경계 설계가 보인다.
1) 본문 파싱 경계: Content-Type은 문자열 비교가 아니다
validator('json', ...)는 개발자에게 매우 편하다. 하지만 이 편함은 “요청이 JSON이라고 올바르게 선언됐다”는 전제 위에 있다. 테스트에서는 보통 Content-Type: application/json만 넣는다. 실제 클라이언트, SDK, 프록시, 테스트 도구는 Application/JSON, application/json; charset=UTF-8, 혹은 예상치 못한 parameter를 보낼 수 있다.
좋은 validator는 너무 느슨해서 아무거나 JSON으로 읽어도 안 되고, 너무 빡빡해서 스펙상 같은 media type을 거부해도 안 된다. 그래서 정규화는 보안과 호환성 사이의 균형이다.
2) 배포 어댑터 경계: Lambda event는 HTTP 요청이 되기 전의 재료다
Hono 문서는 AWS Lambda에서 handle(app)로 앱을 내보내고, c.env.event.requestContext 같은 Lambda 원본 event에도 접근할 수 있다고 설명한다. 이 말은 두 층이 공존한다는 뜻이다.
- 앱 코드는 Hono의
Request/Context를 본다. - 어댑터 코드는 API Gateway, Lambda Function URL, payload format 1.0/2.0의 차이를 본다.
AWS 문서는 payload format 2.0이 multiValueHeaders를 없애고 중복 헤더를 쉼표로 합치며, rawPath와 cookies를 추가한다고 설명한다. 이런 차이를 앱 레벨에서 매번 직접 처리하지 않으려고 adapter를 쓰는 것이다. 따라서 adapter의 event detection은 제품 안정성의 일부다.
3) “빈 값” 경계: empty string은 없음이 아니다
serve-static의 “empty string content를 found로 취급”하는 수정도 의미가 있다. 웹앱에서는 빈 파일이나 빈 응답이 실제로 존재한다.
- feature flag 설정 파일이 빈 JSON 또는 빈 텍스트로 배포될 수 있다.
- robots.txt, health marker, placeholder asset처럼 내용보다 존재 여부가 중요한 파일이 있다.
- 204와 200 빈 body는 의미가 다르다.
if (!content)처럼 falsy 검사로 존재 여부를 판단하면 '', 0, false 같은 값이 “없음”으로 오인된다. 이번 수정은 “값의 존재”와 “값의 내용”을 분리해야 한다는 오래된 기본기를 다시 보여준다.
4. 개발자 관점 해석
웹앱 팀은 이런 릴리스를 보고 “Hono를 업데이트해야 하나?”에서 멈추지 말고, 우리 코드의 입력 경계가 같은 실수를 하고 있는지 봐야 한다.
첫째, 파서는 스키마 검증보다 앞선다. Zod나 Valibot으로 body schema를 잘 짜도, body parser가 {}를 넘겨주면 스키마는 이미 잘못된 재료를 보고 있다. 따라서 request validation 테스트에는 “잘못된 shape”뿐 아니라 “애매한 Content-Type”도 들어가야 한다.
둘째, adapter 테스트는 unit test가 아니라 contract test에 가깝다. Lambda, Cloudflare Workers, Bun, Node, Vercel Functions는 모두 웹 요청을 다르게 포장한다. 프레임워크가 대부분 숨겨 주지만, 인증 헤더, cookie, query string, binary body, WebSocket protocol 같은 지점은 제품 기능과 직접 연결된다.
셋째, AI 코딩 에이전트에게도 경계 조건을 명시해야 한다. 에이전트는 보통 happy path 예제를 잘 만든다. 하지만 Content-Type 대소문자, payload format 1.0/2.0, 빈 정적 파일, 중복 헤더 같은 테스트는 지시하지 않으면 빠뜨리기 쉽다. “validator 테스트 추가해줘”보다 “body parser가 application/json; charset=UTF-8과 Application/JSON을 어떻게 처리하는지 테스트해줘”가 훨씬 낫다.
넷째, 런타임 추상화는 디버깅 로그를 없애는 이유가 아니다. adapter가 잘못 판단하면 앱 라우터 안에서는 이미 변환된 요청만 보인다. 따라서 최소한 오류 로그에는 원본 event의 version, requestContext의 핵심 필드, normalized content type, route path를 남길 수 있어야 한다. 민감한 header나 body를 그대로 찍으라는 뜻은 아니다. 분류에 필요한 메타데이터만 남기라는 뜻이다.
5. 내 프로젝트에 적용할 체크포인트
오늘 바로 확인할 항목은 다음과 같다.
- Content-Type 테스트를 넓힌다.
application/json,Application/JSON,application/json; charset=utf-8, 누락된 Content-Type, 잘못된text/plain을 모두 테스트한다. - body parser 실패와 schema 실패를 구분한다. “본문을 파싱하지 못함”, “media type이 지원되지 않음”, “JSON shape가 틀림”은 서로 다른 400/415 메시지로 나누는 편이 디버깅에 좋다.
- 서버리스 event fixture를 저장한다. API Gateway payload format 1.0, 2.0, Lambda Function URL에서 실제로 들어온 event 샘플을 민감정보 제거 후 test fixture로 남긴다.
- 빈 응답/빈 파일을 테스트한다. 정적 파일, 설정 파일, feature flag, health marker에서
''를 없음으로 오해하는 코드가 있는지 찾는다. - 중복 헤더와 cookie를 확인한다. AWS payload format 2.0은 중복 헤더를 쉼표로 합치고 cookie를 배열로 다룬다. 인증/세션 로직이 이 차이를 가정하고 있는지 확인한다.
- 원본 event 메타데이터를 안전하게 로깅한다.
version,requestContext.http.method, path, normalized media type 정도는 남기되 token, cookie, body는 마스킹한다. - 에이전트 작업 지시문에 경계 조건을 넣는다. “Hono route를 만들어줘”가 아니라 “Content-Type 변형, Lambda v1/v2 fixture, empty body를 포함한 테스트까지 만들어줘”라고 적는다.
6. 오늘 10분 액션
아래처럼 아주 작은 테스트 목록을 추가해보자.
import { describe, expect, it } from 'vitest'
import { Hono } from 'hono'
import { validator } from 'hono/validator'
const app = new Hono()
app.post(
'/json',
validator('json', (value, c) => {
if (!('name' in value)) return c.json({ error: 'invalid body' }, 400)
return value
}),
(c) => c.json(c.req.valid('json'))
)
describe('request input boundary', () => {
it.each([
'application/json',
'Application/JSON',
'application/json; charset=utf-8',
])('accepts JSON media type variant: %s', async (contentType) => {
const res = await app.request('/json', {
method: 'POST',
headers: { 'Content-Type': contentType },
body: JSON.stringify({ name: 'study-room' }),
})
expect(res.status).toBe(200)
})
it('does not silently accept missing content-type', async () => {
const res = await app.request('/json', {
method: 'POST',
body: JSON.stringify({ name: 'study-room' }),
})
expect([400, 415]).toContain(res.status)
})
})
이 테스트의 목적은 Hono 내부 구현을 다시 검증하는 것이 아니다. 우리 팀의 API 계약이 “어떤 입력을 받아들이고, 어떤 입력을 거절하며, 어떤 에러 메시지를 줄 것인가”를 코드로 남기는 것이다.
시간이 5분 더 있다면 Lambda event fixture 하나를 저장하자.
fixtures/aws/http-api-v2-event.redacted.json
fixtures/aws/rest-api-v1-event.redacted.json
그리고 handler test에서 두 fixture가 같은 route, header, cookie 의미로 변환되는지 확인한다. 이 작은 습관이 나중에 “로컬에서는 되는데 API Gateway 뒤에서는 안 된다”를 줄인다.
7. 더 볼 자료
- honojs/hono v4.12.28
- Hono Validation
- Hono on AWS Lambda
- AWS API Gateway HTTP API Lambda proxy integrations
- MDN: Content-Type header
중복 회피 메모
로컬 content/generated와 Supabase 최근 web_app_dev 글을 확인했다. 최근 글은 Next.js 인증/streaming 경계, Server Actions POST 계약, Cache Components와 Turbopack, React Compiler, Supabase OAuth/RLS/Branching, Hono SSR request context 격리와 Hono 보안 릴리스를 다뤘다. 이번 글은 Hono를 다시 다루지만 CORS, body size, path traversal, JSX SSR context, escaping 문제가 아니라 v4.12.28의 Content-Type 정규화, AWS Lambda V2 event 감지, empty static content 처리를 계기로 서버리스 어댑터와 입력 계약 테스트라는 다른 관점에 집중한다.
핵심 출처
로그인하면 이 글을 북마크하고, 나만 보는 한 줄 메모를 남길 수 있어요.
댓글 0
최신순 ▾혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)