API 핸들러 패턴
API 핸들러 패턴과 횡단 관심사
HTTP API 를 짜다 보면 핸들러 본체보다 그 주변 (인증·검증·로깅·에러 변환·rate limit) 코드가 더 많아질 때가 있습니다. 이 횡단 관심사를 어디에 어떻게 두느냐가 일관성과 유지비를 가릅니다.
1. 프레임워크별 메커니즘
횡단 관심사를 표현하는 메커니즘은 프레임워크마다 이름이 다릅니다.
| 프레임워크 | 메커니즘 | 형태 |
|---|---|---|
| Express (Node) | middleware | (req, res, next) => ... |
| Koa | middleware | async (ctx, next) => ... |
| Spring MVC | HandlerInterceptor · Filter |
클래스 + 어노테이션 |
| FastAPI | Depends · middleware |
함수 의존 + ASGI middleware |
| NestJS | Interceptor · Guard · Pipe | 데코레이터 + 클래스 |
| Next.js Route Handlers | wrapper 함수 | withAuth(handler) 같은 합성 |
| Hono | middleware | app.use(...) |
표준 응답 형식 중 하나는 RFC 7807 — Problem Details for HTTP APIs (2016, 후에 RFC 9457 로 개정) 입니다. application/problem+json 콘텐츠 타입과 type · title · status · detail · instance 필드를 정의합니다.
2. 미들웨어 체인 (Express 모델)
요청은 등록 순서대로 미들웨어를 통과하고 각 미들웨어가 next() 를 호출하면 다음으로 넘어갑니다. 응답을 직접 보내거나 에러를 던지면 거기서 체인이 끊깁니다.
app.use(requestId)
app.use(logger)
app.use(authenticate)
app.use('/admin', authorize('admin'))
app.use(routes)
app.use(errorHandler) // (err, req, res, next) — 에러 전용
Koa 는 같은 모델을 async/await 로 다시 표현했습니다. "양파 모델" 이라고 부르는 이유는 응답 후에도 같은 미들웨어가 깨어나 마무리 처리를 할 수 있기 때문입니다.
3. Spring 의 Interceptor · Filter
Servlet Filter 가 가장 바깥, HandlerInterceptor 가 컨트롤러 가까이에서 동작합니다. @RestControllerAdvice 는 예외를 한 곳에서 변환합니다.
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
ResponseEntity<ProblemDetail> notFound(NotFoundException e) {
var p = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
return ResponseEntity.of(p).build();
}
}
Spring 6 부터 ProblemDetail 클래스가 RFC 7807 응답을 표준화해서 만듭니다.
4. FastAPI 의 Depends + middleware
Depends 는 핸들러 진입 직전에 평가되며 인증·DB 세션·검증을 담습니다. app.add_middleware 는 ASGI 레벨의 전역 처리를 담습니다. FastAPI 는 HTTPException 외에 사용자 정의 예외를 app.exception_handler 로 변환할 수 있습니다.
5. Next.js wrapper
Next.js App Router 의 route.ts 는 함수입니다. 합성으로 횡단 관심사를 더합니다.
export const GET = withAuth(withLogging(async (req) => {
return NextResponse.json({ ok: true })
}))
함수 합성은 데코레이터·클래스를 쓰지 않아 가볍지만 순서·타입을 손으로 관리해야 합니다.
6. RFC 7807 응답
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"status": 403,
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc"
}
API 클라이언트가 응답을 파싱할 때 표준 필드를 가정할 수 있다는 것이 강점입니다. errors[] 같은 자체 필드를 더하는 것도 사양상 허용됩니다.
7. 함수 합성 vs 클래스 데코레이터
- 함수 합성 (Express · Hono · Next wrapper) — 흐름이 위에서 아래로 보입니다. 의존이 명시적입니다. 깊어지면 들여쓰기가 길어집니다.
- 클래스 데코레이터 (NestJS · Spring) — 메타데이터로 선언합니다. 핸들러가 짧아집니다. 어노테이션의 적용 순서·우선순위가 암묵적일 수 있습니다.
- dependency 주입 (FastAPI
Depends) — 시그니처에 의존이 적힙니다. 합성과 데코레이터의 중간 형태입니다.
어느 쪽이든 횡단 관심사가 한 곳에 모이고 핸들러는 본체에 집중한다는 목표는 같습니다.
8. ApiError 표준 응답 클래스
자체 ApiError 클래스를 두는 패턴은 흔합니다.
class ApiError extends Error {
constructor(public status: number, public code: string, public detail?: string) { super(detail) }
}
throw 하면 전역 핸들러가 problem+json 으로 변환합니다. RFC 7807 의 type 을 도메인 코드와 매핑해 두면 클라이언트가 분기할 수 있습니다.
9. 미들웨어 순서
대체로 다음 순서가 무난합니다.
① 요청 ID 부여 (관측성)
② 보안 헤더 (Helmet 같은 것)
③ CORS
④ 로깅
⑤ body 파싱
⑥ 인증
⑦ 인가 (라우트별)
⑧ 검증 (스키마)
⑨ 라우트 핸들러
⑩ 에러 핸들러 (가장 마지막)
Rate limit 은 미들웨어 자리입니다. Redis 기반의 토큰 버킷·sliding window 가 흔합니다 (express-rate-limit · @upstash/ratelimit · Spring 의 Bucket4j).
10. 자주 걸리는 자리
에러 미들웨어가 비동기 throw 를 못 잡는 경우 — Express 4 는 async 핸들러의 reject 를 자동 전파하지 않습니다. express-async-errors 같은 보조나 try/catch wrapper 가 필요합니다. Express 5 에서 개선됐습니다.
스택 너머의 응답 종료 — 한 미들웨어가 응답을 보낸 뒤 다음 미들웨어가 또 보내면 ERR_HTTP_HEADERS_SENT 가 납니다. next() 호출 시점·return 을 정리합니다.
클라이언트 IP 와 프록시 — Caddy/nginx 뒤에 있을 때 X-Forwarded-For 신뢰 설정이 빠지면 rate limit 이 프록시 IP 한 개로 묶입니다.
에러 메시지의 정보 노출 — detail 에 스택 트레이스·DB 메시지가 그대로 들어가지 않도록 운영 환경에서 마스킹합니다.
하고픈 말
핸들러 본체보다 횡단 관심사가 코드의 절반이 되는 자리에서는 wrapper 또는 미들웨어로 묶는 가치가 분명히 보입니다. 처음에는 직접 try/catch 로 시작해 패턴이 반복될 때 추상으로 옮겨가는 흐름이 가장 안전합니다.
Next
- jobs-apscheduler
- typeorm-readonly
RFC 7807 · RFC 9457 (개정) · Express middleware · Spring RestControllerAdvice · FastAPI Dependencies · NestJS Interceptors · Hono Middleware 를 참고합니다.