본문으로 건너뛰기
Community실패 사례

여러 파일에 흩어진 NestJS 400 버그를 챗봇 AI가 끝까지 못 짚은 이유

서동현

@bigdata_seo

POST를 날릴 때마다 400이 떠서 며칠을 잡고 있었다. 단위테스트는 전부 green이라 더 미칠 노릇이었다. 원인을 잡고 보니 NestJS의 pipe 적용 범위 문제였는데, 그 과정에서 AI 채팅 디버깅의 한계를 제대로 느꼈다.

증상

컨트롤러 메서드에 @UsePipes(ZodValidationPipe)를 붙여놨다. 의도는 request body만 zod 스키마로 검증하는 거였는데, 이 데코레이터는 같은 핸들러의 @Param/@Query까지 같은 스키마로 파싱하려 든다.

@Post(':orderId/items')
@UsePipes(new ZodValidationPipe(CreateItemSchema))
create(
  @Param('orderId') orderId: string,   // 이것도 CreateItemSchema로 파싱됨
  @Body() body: CreateItemDto,
) { ... }

orderId(string)를 body용 객체 스키마로 검증하니 당연히 실패하고, 정상 요청도 전부 400으로 떨어졌다. 단위테스트는 서비스 레이어만 보고 pipe 바인딩을 안 거쳐서 green이었다.

400 Bad Request
{ "message": "Validation failed", "path": ["orderId"] }

AI 채팅이 헛다리만 짚은 이유

에러 메시지와 컨트롤러 코드를 챗봇에 붙여넣으니 "DTO에 필드가 빠졌네요", "ValidationPipe를 글로벌로 빼세요" 같은 답을 아주 자신만만하게 줬다. 전부 헛다리였다.

원인이 한 파일에 안 모여 있었던 게 핵심이다. @UsePipes 데코레이터, @Param 바인딩, zod 스키마 정의, 글로벌 파이프 설정이 각각 다른 파일에 흩어져 있었다. 채팅에 붙여넣을 수 있는 건 그 중 한두 조각뿐이고, AI는 자기가 본 조각 안에서만 그럴듯한 가설을 세운다. 파이프가 param 바인딩까지 가로챈다는 사실은 컨트롤러 코드 한 장만 봐서는 절대 안 보인다.

결국 잡은 방법

supertest로 e2e를 한 번 돌리고 로그를 따라갔다. 정상 요청인데도 validation이 orderId에서 떨어지는 걸 로그로 확인하고서야 "아, pipe가 param까지 먹는구나"가 보였다. 해결은 메서드레벨 @UsePipes 대신 @Body()에 파이프를 직접 물리는 것이었다.

create(
  @Param('orderId') orderId: string,
  @Body(new ZodValidationPipe(CreateItemSchema)) body: CreateItemDto,
) { ... }

교훈

단일 파일 안에서 완결되는 버그는 AI 채팅이 잘 짚는다. 하지만 데코레이터 적용 범위처럼 여러 파일과 프레임워크 동작에 걸친 버그는, 컨텍스트를 다 물려줘도 조각 단위로만 추론해서 엉뚱한 데를 짚는다. 이럴 땐 정적 분석이나 채팅보다 실제 요청을 쏘고(e2e/supertest) 런타임 로그를 따라가는 게 빠르다. 재현되는 실패 요청 하나가 그럴듯한 가설 열 개보다 낫다.

이 글에 대한 Q&A

이 글에 질문하기

이 글에서 궁금한 점이 있다면 먼저 질문을 남겨보세요.

댓글

첫 댓글을 남겨보세요.