엑셀 파싱 시 대량 객체 생성으로 힙 메모리 고갈과 OOM

📅 2월 25, 2026 👤 Roxanna
프로그램의 메모리 사용량이 시간이 지남에 따라 지속적으로 증가하여 결국 OutOfMemoryError가 발생하는 메모리 누수 진단 과정을 단계별로 설명하는 플로우차트입니다.

증상 진단: 메모리 누수와 OOM(OutOfMemoryError) 발생 패턴

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)로 이어집니다, 근본적 원인은 ‘한 번에 모든 데이터를 메모리에 탑재’하는 설계 방식에 있습니다.

디지털 붕괴와 데이터 손실을 상징하는 3D 와이어프레임 큐브 모델로, 빨갛게 빛나며 부풀어 오른 부분에서 모래가 흘러나와 아래의 엉킨 쇠사슬 더미에 쌓여 있습니다.

해결 방법 1: 이벤트 기반 스트리밍(SAX) 파서로 전환

가장 근본적이고 효과적인 해결책은 객체 모델이 아닌 이벤트 기반 스트리밍 파서를 사용하는 것입니다. 이러한 apache POI에서는 XSSFSXSSF를 구분해야 합니다.

주의사항: 스트리밍 방식은 ‘읽기 전용(Read-Only)’에 최적화되어 있습니다, 셀 스타일 읽기나 수식 평가 등 고급 기능이 제한되며, 임의의 행(row) 접근(random access)이 불가능합니다. 원본 파일을 변경해야 한다면, SXSSF 방식을 고려하십시오.

Apache POI의 XSSF SAX (Event API)를 사용한 구현 단계는 다음과 같습니다.

  1. 필수 패키지 임포트: org.apache.poi.xssf.eventusermodel.XSSFReaderorg.xml.sax.helpers.DefaultHandler를 포함시킵니다.
  2. OPC 패키지 열기: OPCPackage를 이용해 파일을 스트림으로 엽니다. 이는 파일 전체를 메모리에 올리지 않습니다.
    OPCPackage pkg = OPCPackage.open(new File("대용량_파일.xlsx").getPath());
  3. SAX 파서 설정: XSSFReader를 생성하고, 시트 데이터를 처리할 커스텀 DefaultHandler를 작성합니다. 핸들러 내 startElement, endElement, characters 메서드를 오버라이드하여 행과 셀 데이터를 청크 단위로 처리합니다.
  4. 시트별 스트리밍 처리: XSSFReader.getSheetsData()로 각 시트의 InputStream을 얻어, SAX 파서에 전달합니다. 데이터 처리가 끝난 행(Row)과 셀(Cell) 객체는 즉시 참조 해제되어 GC 대상이 됩니다.
  5. 리소스 정리: 모든 처리 후 pkg.close()를 호출하여 스트림을 안전하게 닫습니다.

이 방식은 파일 크기에 관계없이 일정 수준의 매우 낮은 힙 메모리만 사용하며, OOM 위험을 근본적으로 제거합니다.

프로그램의 메모리 사용량이 시간이 지남에 따라 지속적으로 증가하여 결국 OutOfMemoryError가 발생하는 메모리 누수 진단 과정을 단계별로 설명하는 플로우차트입니다.

스트리밍 방식의 장단점 비교

  • 장점: 극도로 낮은 메모리 사용량, 대용량 파일 처리 가능, 일정한 처리 속도 유지.
  • 단점: 읽기 전용 작업에 한함, 복잡한 셀 스타일이나 수식 접근 불편, 코드 작성 복잡도 증가.

해결 방법 2: SXSSF(Streaming Usermodel API)를 활용한 쓰기 최적화 확장

대용량 Excel 파일을 ‘생성’해야 하는 경우, Apache POI의 SXSSF 모듈을 사용합니다. SXSSF는 내부적으로 ‘슬라이딩 윈도우’ 방식을 채택하여, 지정된 크기(예: 100행)만 메모리에 유지하고 디스크에 임시 저장합니다.

  1. SXSSF Workbook 생성: 창 크기(메모리에 보관할 행 수)를 지정합니다.
    SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 행 100개만 메모리 보관
  2. 데이터 쓰기: 기존 XSSF와 유사한 API(createSheet(), createRow())로 데이터를 작성합니다. 지정된 창 크기를 초과하는 이전 행들은 자동으로 디스크로 플러시되어 메모리에서 해제됩니다.
  3. 파일 작성 및 정리: FileOutputStream으로 파일을 쓰고, 반드시 workbook.dispose()를 호출하여 임시 파일을 삭제합니다. 이 단계를 생략하면 디스크에 임시 파일이 누적됩니다.

SXSSF는 쓰기 작업 시 메모리 고갈을 방지하는 표준 해법입니다. 읽기 작업에는 SAX 방식을, 쓰기 작업에는 SXSSF 방식을 적용하는 것이 일반적입니다.

해결 방법 3: JVM 힙 메모리 설정 최적화 및 코드 레벨 개선

아키텍처 변경이 어려운 경우, 임시 조치로 JVM 설정을 조정하고 코드 패턴을 개선할 수 있습니다. 이는 근본 해결책이 아니며, 시스템 리소스의 한계 내에서 시간을 벌어주는 방법입니다.

  1. JVM 힙 메모리 증가: 애플리케이션 실행 옵션을 조정합니다. 일례로, -Xmx4g -Xms2g는 최대 힙을 4GB, 초기 힙을 2GB로 설정합니다. 그러나 물리 메모리 한계와 GC로 인한 긴 일시 정지(STW) 위험이 따릅니다.
  2. 명시적 가비지 컬렉션 유도 및 참조 해제: 객체 사용 후 명시적으로 null을 할당하고, 필요시 System.gc()를 고려합니다. (주의: System.gc()는 힌트에 불과하며, 성능에 예측 불가능한 영향을 줄 수 있음)
  3. 불필요한 객체 생성 최소화: 셀 값을 읽을 때 getStringCellValue() 대신 cell.getRawValue()를 고려하여 중간 문자열 객체 생성을 줄입니다. 반복문 내에서의 불필요한 Date, DecimalFormat 객체 생성도 피해야 합니다.
  4. 캐시 설정 비활성화: POI의 org.apache.poi.util.POILogger 레벨을 조정하거나, org.apache.poi.ss.usermodel.DataFormatter와 같은 컴포넌트의 캐싱 동작을 검토하여 불필요한 메모리 점유를 막습니다.

주의사항: 데이터 무결성과 처리 안정성 확보

메모리 문제 해결 과정에서 데이터 정합성과 처리의 안정성을 해쳐서는 안 됩니다. 다음 사항을 준수해야 합니다.

  • 스트리밍 처리 중 예외 처리: SAX 파싱 중 발생하는 예외는 반드시 캐치하여, 이미 처리된 데이터를 롤백하거나 로깅할 수 있는 메커니즘을 마련합니다. 스트림이 비정상 종료되지 않도록 합니다.
  • 임시 파일 관리: SXSSF 사용 시 dispose() 메서드 호출을 보장합니다. finally 블록이나 try-with-resources 구문을 활용하여 리소스 누수를 방지합니다.
  • 점진적 롤아웃: 새로운 스트리밍 로직을 실제 서비스에 적용하기 전, 반드시 스테이징 환경에서 다양한 크기와 형식의 파일로 충분한 부하 테스트를 수행합니다. 기존 객체 모델과의 결과값 비교를 통해 데이터 파싱 정확성을 검증해야 합니다.

전문가 팁: 하이브리드 접근법과 모니터링
완전한 스트리밍으로 전환하기 어려운 복잡한 로직이 있다면, ‘하이브리드 접근법’을 고려하십시오. 파일의 첫 번째 시트 또는 메타데이터는 스트리밍으로 빠르게 훑고, 일례로 필요한 특정 범위의 데이터(예: 특정 조건을 만족하는 5만 행)만 선별적으로 표준 객체 모델로 로드할 수 있습니다. 또한, 반드시 애플리케이션 레벨의 모니터링(예: Micrometer, Prometheus Grafana 대시보드)을 통해 GC 빈도, 힙 사용량, 처리 행 수를 실시간으로 관찰하십시오. OOM은 단순한 에러가 아니라 시스템 설계 결함의 최종 신호입니다. 지표를 통해 문제가 재발하지 않도록 사전에 패턴을 파악하는 것이 장기적인 시스템 안정성 확보의 핵심입니다.

관련 글