API 요청 시 발생하는 알 수 없는 500 오류
폼 데이터를 저장하는 기능을 개발하던 중, API 요청이 실패하며 다음과 같은 응답을 받는 상황이 발생할 수 있습니다.
{
"status": 500,
"error": "Invalid JSON format",
"success": false
}
요청 페이로드를 확인해 보면 다음과 같이 비어 있는 문자열이 포함된 것을 볼 수 있습니다.
{
"documentId": 2048,
"documentName": "초안 문서",
"classId": "",
"teamId": "",
"attachments": []
}
사용자가 드롭다운에서 값을 선택하지 않았을 때, 프론트엔드에서 빈 문자열("")이 전송된 것이 문제의 발단입니다.
오류의 근본 원인
백엔드 API의 데이터 전송 객체(DTO)가 다음과 같이 정의되어 있다고 가정해 봅시다.
@Getter
@Setter
public class DocumentRequest {
private Long documentId;
private String documentName;
private Long classId;
private Long teamId;
}
Java의 Jackson 라이브러리는 JSON을 역직렬화할 때 빈 문자열 ""을 Long이나 Integer 같은 숫자 타입으로 변환하지 못하며, 이 과정에서 예외를 발생시킵니다.
데이터 타입별 빈 값 매핑 결과
| 프론트엔드 전송 값 | 백엔드 필드 타입 | 처리 결과 |
|---|---|---|
null | Long | 성공 (값: null) |
"" | Long | 실패 (JSON 파싱 예외) |
"200" | Long | 성공 (값: 200L) |
200 | Long | 성공 (값: 200L) |
| 필드 누락 | Long | 성공 (값: null) |
"" | String | 성공 (값: "") |
"" | Integer | 실패 (JSON 파싱 예외) |
모든 숫자 기반 타입(Long, Integer, Double, BigDecimal 등)은 빈 문자열을 허용하지 않는다는 점을 인지해야 합니다.
프론트엔드 해결 전략
1. 페이로드 재귀적 정제 (권장)
요청을 보내기 전에 객체 내부의 모든 빈 문자열을 null로 변환하는 유틸리티 함수를 작성합니다.
const sanitizePayload = (data) => {
if (Array.isArray(data)) {
return data.map(sanitizePayload);
}
if (data !== null && typeof data === 'object') {
return Object.keys(data).reduce((acc, key) => {
const val = data[key];
acc[key] = val === '' ? null : sanitizePayload(val);
return acc;
}, {});
}
return data;
};
// 활용 예시
const rawPayload = {
documentId: 2048,
documentName: '초안 문서',
classId: '',
teamId: '',
};
const cleanPayload = sanitizePayload(rawPayload);
await apiService.submit(cleanPayload);
2. 개별 필드 명시적 처리
대상 필드가 적다면 각 필드에 대해 직접 조건문을 적용할 수 있습니다.
const payload = {
documentId: formData.documentId,
documentName: formData.documentName,
classId: formData.classId === '' ? null : formData.classId,
teamId: formData.teamId === '' ? null : formData.teamId,
};
3. HTTP 클라이언트 인터셉터 활용
Axios와 같은 HTTP 클라이언트의 요청 인터셉터에 정제 로직을 주입하여 전역적으로 적용합니다.
httpClient.interceptors.request.use((reqConfig) => {
if (reqConfig.data && reqConfig.headers['Content-Type']?.includes('application/json')) {
reqConfig.data = sanitizePayload(reqConfig.data);
}
return reqConfig;
});
백엔드 설정 변경 (비권장)
Jackson의 설정을 변경하여 빈 문자열을 null 객체로 수용하도록 할 수 있으나, 전역 설정 변경은 부작용을 초래할 수 있으므로 권장하지 않습니다.
// 권장하지 않는 방식
objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
이 방식은 다른 정상적인 API의 동작을 방해할 수 있으며, 프론트엔드의 데이터 타입 불일치 문제를 은폐하게 만듭니다.
오류 예방을 위한 개발 가이드라인
1. 상태 초기화 시 null 사용
폼 상태를 초기화할 때 빈 문자열 대신 null을 할당합니다.
// 잘못된 방식
const formState = reactive({
classId: '',
teamId: ''
});
// 올바른 방식
const formState = reactive({
classId: null,
teamId: null
});
2. UI 컴포넌트 클리어 이벤트 처리
셀렉트 박스 등의 UI 컴포넌트에서 값을 지울 때 명시적으로 null을 할당하도록 이벤트를 바인딩합니다.
<template>
<Select
v-model="formState.classId"
clearable
@clear="formState.classId = null"
/>
</template>
3. TypeScript 인터페이스를 통한 타입 강제
타입 정의를 통해 빈 문자열 할당을 컴파일 단계에서 차단합니다.
interface DocumentForm {
documentId: number;
documentName: string;
classId: number | null;
teamId: number | null;
}
const formState: DocumentForm = {
documentId: 2048,
documentName: '초안 문서',
classId: null,
// classId: '', // 컴파일 오류 발생
};
주의해야 할 엣지 케이스
폼 데이터 리셋
폼을 초기화하는 로직에서 빈 문자열을 할당하지 않도록 주의해야 합니다.
const resetForm = () => {
formState.classId = null; // '' 대신 null 사용
};
URL 쿼리 파라미터 파싱
라우터의 쿼리 파라미터는 값이 비어있을 경우 빈 문자열로 파싱될 수 있으므로, 이를 null로 변환해 주어야 합니다.
// URL: /documents?classId=
const rawClassId = route.query.classId;
const classId = rawClassId === '' ? null : rawClassId;
서드파티 UI 라이브러리의 기본 동작
일부 UI 라이브러리는 드롭다운의 '지우기' 버튼을 클릭했을 때 null이 아닌 빈 문자열을 v-model에 바인딩하는 경우가 있습니다. 이러한 라이브러리의 고유 동작을 파악하고 change 또는 clear 이벤트에서 값을 보정해 주어야 합니다.