
Vite와 Spring Boot를 이용하여 파일 업로드 기능을 구현하던 중, 로컬 환경에서는 완벽하게 동작하던 기능이 ngrok을 통한 외부 접속 환경에서만 간헐적으로 실패하는 현상이 발생했습니다.
org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request
...
Caused by: org.apache.catalina.connector.ClientAbortException: java.io.EOFException
에러 로그는 백엔드 컨트롤러 진입 전, 서블릿 레이어에서 멀티파트 데이터를 파싱하다가 클라이언트가 연결을 강제로 끊었음을 가리키고 있었습니다.
단순히 설정을 바꾸는 것이 아니라, 요청의 흐름을 계층별로 분리하여 원인을 추적했습니다.
Vite Proxy가 멀티파트 데이터를 백엔드로 전달하는 과정에서 데이터 스트림이 끊길 가능성.content-length를 강제 지정했으나 증상 지속. ngrok의 무료 플랜 대역폭 제한이나 네트워크 홉 증가로 인해 데이터 전송이 불안정해진 것으로 판단.문제의 핵심은 "복잡한 프록시 체인과 데이터 스트리밍의 불일치"였습니다.
Browser → ngrok → Vite Proxy → Spring Boot라는 3단계 계층을 거치며 데이터가 전달됩니다.EOFException이 발생한 것입니다.개발 편의를 위한 Vite Proxy가 오히려 데이터 정합성을 해치고 있었으므로, 환경에 따라 API 엔드포인트를 동적으로 분리하여 프록시 체인을 단순화했습니다.
// 환경 변수를 활용한 엔드포인트 전략 분리
const getBaseUrl = () => {
// ngrok 환경일 경우 프록시를 거치지 않고 백엔드 ngrok 터널로 직접 통신
if (window.location.hostname.includes('ngrok-free.app')) {
return import.meta.env.VITE_BACKEND_DIRECT_URL;
}
return '/api'; // 로컬 개발 시에는 기존처럼 Vite 프록시 활용
};
이후 직접 통신 방식을 통해 데이터 유실 없이 대용량 파일도 안정적으로 업로드됨을 확인했습니다.
단순히 "에러를 고쳤다"에서 끝나지 않고, 실제 운영 환경에서 발생할 수 있는 변수들을 고려하여 시스템을 보완했습니다.
이 부분은 서비스의 신뢰도와 관찰 가능성을 높이는 데 초점을 맞추었습니다.
사용자가 업로드 중 브라우저를 닫는 행위는 장애가 아닌 '정상적 이탈'입니다.
이를 시스템 에러와 분리하여 로그 신뢰도를 높였습니다.
@ExceptionHandler(ClientAbortException.class)
public void handleClientAbort(ClientAbortException e) {
// 경고 로그로 분류하여 불필요한 장애 알람 발생 방지
log.warn("File upload aborted by user/network: {}", e.getMessage());
}
불필요한 서버 자원 낭비를 막기 위해 프론트엔드 제어권을 강화했습니다.
beforeunload 이벤트를 통해 업로드 중 페이지 이동 시 경고창 노출.브라우저의 암묵적 중단에 의존하지 않고, 컴포넌트 언마운트 시 네트워크 요청을 명시적으로 취소하여 메모리 누수를 방지합니다.
const controller = new AbortController();
axios.post("/upload", formData, { signal: controller.signal });
// Cleanup function
return () => controller.abort();
파일 업로드는 본질적으로 CPU와 I/O 소모가 큰 작업입니다.
현재의 단일 서버 구조를 넘어선 아키텍처를 설계에 반영할 예정입니다.
"모든 네트워크 에러가 서버의 잘못은 아니다. 하지만 모든 에러는 서버에서 우아하게 처리되어야 한다."
이번 이슈를 통해 네트워크 홉이 늘어날수록 발생할 수 있는 Side Effect를 깊이 이해하게 되었습니다.
또한, 로그 분석 시 단순 에러 발생 여부보다 "어떤 레이어에서, 왜 끊겼는가"를 추적하는 정교한 디버깅 프로세스의 중요성을 다시 한번 체감했습니다.