프론트엔드 빈 문자열이 유발하는 백엔드 Long 타입 JSON 파싱 오류 및 해결책

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 같은 숫자 타입으로 변환하지 못하며, 이 과정에서 예외를 발생시킵니다.

데이터 타입별 빈 값 매핑 결과

프론트엔드 전송 값 백엔드 필드 타입 처리 결과
nullLong성공 (값: null)
""Long실패 (JSON 파싱 예외)
"200"Long성공 (값: 200L)
200Long성공 (값: 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 이벤트에서 값을 보정해 주어야 합니다.

태그: java Jackson JavaScript axios TypeScript

6월 3일 16:23에 게시됨