문제의 출발점
Excel 업로드 장애를 볼 때 가장 먼저 확인하기 쉬운 값은 파일 크기와 화면에 보이는 데이터 행 수다. 하지만 두 값만으로 서버가 실제로 처리할 작업량을 설명할 수 없는 경우가 있다.
이번 사례의 파일은 업무 데이터보다 훨씬 넓은 약 6만 개의 빈 행 영역을 포함하고 있었다. 업로드 처리 중 JVM에서 GC overhead limit exceeded가 발생했고, 단순히 Heap을 늘리는 대신 외부 입력의 크기와 파싱 종료 조건을 함께 제한했다.
화면에 보이는 행 수와 파서의 처리 범위는 다르다
사용자가 Excel을 열었을 때 몇 줄만 보인다고 해서 파일 내부의 사용 범위도 그만큼 작다는 뜻은 아니다. 값이 지워진 뒤 서식이나 셀 구조가 남았거나, 편집 이력 때문에 넓은 영역이 파일에 포함될 수 있다.
Apache POI가 어떤 객체를 언제 만드는지는 파일 형식과 사용 API, 반복 방식에 따라 달라진다. 따라서 이번 원인을 특정 Workbook 구현이나 하나의 메서드 탓으로 단정하지 않았다. 확인한 사실은 다음과 같았다.
- 장애 파일에는 약 6만 개의 빈 행 영역이 있었다.
- 업로드 처리 중
GC overhead limit exceeded가 발생했다. - 유효한 업무 데이터가 끝난 뒤에도 파싱 범위가 계속 이어졌다.
- 불필요한 처리 범위를 제한한 뒤 같은 검증 흐름을 통과했다.
핵심은 “빈 행은 메모리를 사용하지 않는다”는 가정이 안전하지 않다는 점이었다. 셀 값이 비어 있어도 파일 구조를 읽고 행을 순회하며 중간 객체를 만드는 비용은 남을 수 있다.
원인을 입력 크기와 내부 구조로 나눠 봤다
파일 업로드에는 서로 다른 두 가지 크기가 존재한다.
| 구분 | 의미 | 이번 대응 |
|---|---|---|
| 전송 크기 | 서버가 한 번에 받아들이는 파일 자체의 크기 | 3MB 업무 기준 적용 |
| 처리 범위 | 파서가 행과 셀을 순회하며 해석하는 범위 | 빈 행 이후 파싱 종료 |
파일 크기 제한만 적용하면 지나치게 큰 요청은 일찍 거절할 수 있다. 그러나 제한보다 작은 파일도 내부 구조가 비정상적으로 넓으면 많은 행을 순회할 수 있다.
반대로 파싱 종료 조건만 추가하면 내부 구조는 제어할 수 있지만, 애초에 지나치게 큰 파일을 네트워크와 서버가 받아들이는 비용은 남는다. 그래서 두 방어선을 별개의 책임으로 두었다.
Heap 증설을 우선하지 않은 이유
메모리 오류가 발생하면 JVM Heap을 늘리는 방법을 떠올릴 수 있다. 이 방법은 처리 가능한 입력 범위를 넓힐 수 있지만, 불필요한 행을 끝까지 읽는 구조 자체를 바꾸지는 않는다.
이번 문제에서는 다음 이유로 Heap 증설을 실제 해결책으로 사용하지 않았다.
- 정상 업무 데이터가 아니라 비정상적으로 넓은 빈 행 영역이 비용을 만들었다.
- 같은 형태의 파일이 더 커지면 증설한 한도에서도 문제가 반복될 수 있었다.
- 한 요청이 많은 메모리를 점유하면 같은 JVM의 다른 요청에도 영향을 줄 수 있었다.
- 입력 계약과 처리 종료 지점을 명시하는 편이 운영 한계를 예측하기 쉬웠다.
라이브러리 교체나 Streaming API 전환도 가능한 일반적 선택지지만, 이번에 실제 적용한 것으로 확인되는 범위는 3MB 제한과 빈 행 이후 파싱 중단이다.
실제 적용한 두 단계 방어
아래 예시는 실제 소스가 아니라 책임 경계를 설명하기 위한 pseudocode다. 정확한 바이트 상수나 내부 행 판정 코드는 공개하지 않는다.
업로드 요청 수신
if 파일 크기가 업무 제한을 벗어남:
안내 메시지 반환
파싱 시작하지 않음
for 각 행 in Excel:
if 업무상 빈 행으로 판정됨:
이후 파싱 중단
필요한 값만 읽어 업무 객체 생성첫 번째 단계는 입력 파일 자체를 3MB로 제한했다. 두 번째 단계는 빈 행을 만났을 때 이후 파싱을 중단해 필요하지 않은 객체 생성과 메모리 사용을 줄였다.
여기서 “빈 행”은 단순히 셀 하나가 비어 있다는 뜻으로 일반화할 수 없다. 필수 업무 값이 모두 비어 있는지, 합계·주석 행을 데이터로 보아야 하는지 같은 판정은 파일 계약에 맞아야 한다.
빈 행 종료 조건이 항상 안전한 것은 아니다
중간에 빈 행을 두고 그 아래에 다시 유효 데이터가 나오는 파일이라면 첫 빈 행에서 중단하는 방식은 정상 데이터를 누락시킨다. 따라서 이 방식은 빈 행이 데이터 영역의 끝을 의미하는 입력 계약에서만 사용할 수 있다.
외부에서 자유 형식 Excel을 받는 시스템이라면 다른 선택이 필요할 수 있다.
- 연속된 빈 행이 일정 수 이상일 때 종료
- 명시적인 종료 행이나 데이터 범위 사용
- 유효 행 수의 상한 설정
- 템플릿 버전과 필수 헤더 검증
- 처리 시간·행 수·메모리 사용량을 함께 관찰
이 항목들은 재사용 가능한 설계 선택지이며, 이번 작업에서 모두 구현했다는 뜻은 아니다.
검증 기록을 고유 시나리오와 실행 행으로 구분했다
검증에는 3개의 고유 시나리오가 사용됐다. 같은 시나리오를 단계별로 반복했기 때문에 기록은 총 9행이다.
| 단계 | 고유 시나리오 | 기록 | 결과 |
|---|---|---|---|
| 최초 내부 검증 | 3개 | 3행 | N |
| 수정 후 재검증 | 같은 3개 | 3행 | Y |
| 사용자 검증 | 같은 3개 | 3행 | Y |
따라서 이를 “9개 테스트 시나리오가 모두 처음부터 성공했다”고 표현할 수 없다. 최초 내부 검증에서 확인된 문제를 수정한 뒤 재검증과 사용자 검증이 성공했고, 최종 완료 상태가 확인된 범위다.
남는 한계
이번 수정은 확인된 Excel 입력 조건을 사전에 차단한 것이다. 모든 종류의 메모리 오류를 해결하거나 Apache POI의 메모리 사용을 일반적으로 최적화한 작업은 아니다.
3MB도 모든 시스템에 적절한 값은 아니다. 허용해야 하는 데이터량, 동시 업로드 수, JVM 자원과 처리 방식에 따라 기준은 달라진다. 파일 크기가 작다고 내부 구조까지 안전한 것도 아니고, 파일이 크다고 반드시 메모리 오류가 발생하는 것도 아니다.
정리
파일 업로드의 안전성은 전송 용량 하나로 결정되지 않는다. 서버가 실제로 순회할 행·셀 범위와 객체 생성량까지 입력 계약에 포함해야 한다.
이번 개선의 핵심은 메모리 한도를 키워 증상을 늦추는 데 있지 않았다. 받아들일 파일의 크기와 읽기를 멈출 조건을 각각 정의해, 비정상 입력이 JVM 전체의 부담으로 확대되는 경로를 줄인 것이었다.