부동소수점 연산의 정밀도 한계로 재무 데이터의 단수 차이 보정
증상 확인: 재무 보고서에서 발생하는 0.01원 단위의 오차 재무 소프트웨어, ERP 시스템, 또는 자체 개발한...
Excel 파일(가령 .xlsx)을 Apache POI, OpenPyXL 등의 라이브러리로 파싱할 때, java.lang.OutOfMemoryError: Java heap space 에러가 발생합니다. 콘솔 로그나 모니터링 도구를 확인하면, 작업 중 JVM 힙 메모리 사용량이 지속적으로 상승하다가 한계점에 도달하여 애플리케이션이 비정상 종료되는 현상을 관찰할 수 있습니다. 이는 단순히 파일 크기가 커서가 아니라, 라이브러리의 객체 모델이 모든 셀(Cell), 행(Row), 시트(Sheet) 데이터를 메모리에 로드하는 방식 때문입니다. 10만 행 이상의 대용량 파일을 처리할 때 이 증상이 명확히 나타납니다.
전통적인 Excel 파싱 라이브러리는 사용자 편의성을 위해 DOM(Document Object Model) 방식의 API를 제공합니다. 이는 파일을 읽으면서 Workbook, Sheet, Row, Cell 객체를 전체적으로 생성하여 메모리 상에 트리 구조로 유지합니다. 각 객체는 내부 속성(값, 스타일, 수식 등)과 상위/하위 객체에 대한 참조를 보유하고 있어, 파일의 물리적 크기보다 훨씬 큰 메모리를 점유하게 됩니다. 게다가, 개발자가 이러한 객체들을 컬렉션(예: List<Row>)에 보관하거나 불필요하게 캐싱할 경우, 가비지 컬렉션(GC)의 대상에서 벗어나 메모리 누수(Memory Leak)로 이어집니다, 근본적 원인은 ‘한 번에 모든 데이터를 메모리에 탑재’하는 설계 방식에 있습니다.

가장 근본적이고 효과적인 해결책은 객체 모델이 아닌 이벤트 기반 스트리밍 파서를 사용하는 것입니다. 이러한 apache POI에서는 XSSF와 SXSSF를 구분해야 합니다.
주의사항: 스트리밍 방식은 ‘읽기 전용(Read-Only)’에 최적화되어 있습니다, 셀 스타일 읽기나 수식 평가 등 고급 기능이 제한되며, 임의의 행(row) 접근(random access)이 불가능합니다. 원본 파일을 변경해야 한다면, SXSSF 방식을 고려하십시오.
Apache POI의 XSSF SAX (Event API)를 사용한 구현 단계는 다음과 같습니다.
org.apache.poi.xssf.eventusermodel.XSSFReader와 org.xml.sax.helpers.DefaultHandler를 포함시킵니다.OPCPackage를 이용해 파일을 스트림으로 엽니다. 이는 파일 전체를 메모리에 올리지 않습니다.OPCPackage pkg = OPCPackage.open(new File("대용량_파일.xlsx").getPath());XSSFReader를 생성하고, 시트 데이터를 처리할 커스텀 DefaultHandler를 작성합니다. 핸들러 내 startElement, endElement, characters 메서드를 오버라이드하여 행과 셀 데이터를 청크 단위로 처리합니다.XSSFReader.getSheetsData()로 각 시트의 InputStream을 얻어, SAX 파서에 전달합니다. 데이터 처리가 끝난 행(Row)과 셀(Cell) 객체는 즉시 참조 해제되어 GC 대상이 됩니다.pkg.close()를 호출하여 스트림을 안전하게 닫습니다.이 방식은 파일 크기에 관계없이 일정 수준의 매우 낮은 힙 메모리만 사용하며, OOM 위험을 근본적으로 제거합니다.

대용량 Excel 파일을 ‘생성’해야 하는 경우, Apache POI의 SXSSF 모듈을 사용합니다. SXSSF는 내부적으로 ‘슬라이딩 윈도우’ 방식을 채택하여, 지정된 크기(예: 100행)만 메모리에 유지하고 디스크에 임시 저장합니다.
SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 행 100개만 메모리 보관XSSF와 유사한 API(createSheet(), createRow())로 데이터를 작성합니다. 지정된 창 크기를 초과하는 이전 행들은 자동으로 디스크로 플러시되어 메모리에서 해제됩니다.FileOutputStream으로 파일을 쓰고, 반드시 workbook.dispose()를 호출하여 임시 파일을 삭제합니다. 이 단계를 생략하면 디스크에 임시 파일이 누적됩니다.SXSSF는 쓰기 작업 시 메모리 고갈을 방지하는 표준 해법입니다. 읽기 작업에는 SAX 방식을, 쓰기 작업에는 SXSSF 방식을 적용하는 것이 일반적입니다.
아키텍처 변경이 어려운 경우, 임시 조치로 JVM 설정을 조정하고 코드 패턴을 개선할 수 있습니다. 이는 근본 해결책이 아니며, 시스템 리소스의 한계 내에서 시간을 벌어주는 방법입니다.
-Xmx4g -Xms2g는 최대 힙을 4GB, 초기 힙을 2GB로 설정합니다. 그러나 물리 메모리 한계와 GC로 인한 긴 일시 정지(STW) 위험이 따릅니다.System.gc()를 고려합니다. (주의: System.gc()는 힌트에 불과하며, 성능에 예측 불가능한 영향을 줄 수 있음)getStringCellValue() 대신 cell.getRawValue()를 고려하여 중간 문자열 객체 생성을 줄입니다. 반복문 내에서의 불필요한 Date, DecimalFormat 객체 생성도 피해야 합니다.org.apache.poi.util.POILogger 레벨을 조정하거나, org.apache.poi.ss.usermodel.DataFormatter와 같은 컴포넌트의 캐싱 동작을 검토하여 불필요한 메모리 점유를 막습니다.메모리 문제 해결 과정에서 데이터 정합성과 처리의 안정성을 해쳐서는 안 됩니다. 다음 사항을 준수해야 합니다.
dispose() 메서드 호출을 보장합니다. finally 블록이나 try-with-resources 구문을 활용하여 리소스 누수를 방지합니다.전문가 팁: 하이브리드 접근법과 모니터링
완전한 스트리밍으로 전환하기 어려운 복잡한 로직이 있다면, ‘하이브리드 접근법’을 고려하십시오. 파일의 첫 번째 시트 또는 메타데이터는 스트리밍으로 빠르게 훑고, 일례로 필요한 특정 범위의 데이터(예: 특정 조건을 만족하는 5만 행)만 선별적으로 표준 객체 모델로 로드할 수 있습니다. 또한, 반드시 애플리케이션 레벨의 모니터링(예: Micrometer, Prometheus Grafana 대시보드)을 통해 GC 빈도, 힙 사용량, 처리 행 수를 실시간으로 관찰하십시오. OOM은 단순한 에러가 아니라 시스템 설계 결함의 최종 신호입니다. 지표를 통해 문제가 재발하지 않도록 사전에 패턴을 파악하는 것이 장기적인 시스템 안정성 확보의 핵심입니다.
증상 확인: 재무 보고서에서 발생하는 0.01원 단위의 오차 재무 소프트웨어, ERP 시스템, 또는 자체 개발한...
증상 진단: 내부 네트워크에서의 비정상적 세션 활동 내부 직원 계정으로 로그인한 상태에서, 갑자기 파일 서버...
증상: 백업 복구 후 데이터 불일치 또는 비즈니스 로직 오류 클라우드 네이티브 환경에서 데이터베이스(DB) 백업을...