POI에서 DsExcel로: SpreadJS 연동 가이드

POI에서 DsExcel로: SpreadJS 연동 가이드

1. 들어가며: 왜 다시 '엑셀(Excel)'인가?

기업용 엔터프라이즈 솔루션 개발에서 가장 흔하면서도 까다로운 요구사항 중 하나는 단연 **'엑셀(Excel) 처리'**입니다. 실무 현장의 사용자들은 웹 화면에서 화려한 대시보드를 보는 것에 그치지 않고, 복잡한 다중 헤더 그리드 데이터를 조회하고, 그룹별 트리 구조로 서식이 지정된 템플릿을 다운로드 받아 오프라인에서 작업한 뒤, 이를 다시 시스템으로 대량 업로드하기를 원합니다.

오랫동안 자바 백엔드 생태계에서 엑셀 처리의 표준은 사실상 Apache POI였습니다. 오픈소스 생태계의 풍부한 레퍼런스와 무료라는 압도적인 장점 덕분에 대부분의 프로젝트가 POI로 시작합니다. 그러나 다음과 같은 복잡한 비즈니스 요구사항이 결합되는 순간 개발팀은 거대한 기술적 장벽에 부딪히게 됩니다.

  • **복잡한 계층형 다중 헤더(Multi-Header)**와 트리 구조를 지닌 그룹-필드 템플릿 생성

  • 웹 UI에서 SpreadJS와 같은 초고성능 스프레드시트 컴포넌트를 도입했을 때 발생하는 백엔드 간 서식/데이터 동기화 왜곡

  • 단순 값 입출력을 넘어선 SUMPRODUCT, DSUM 등 고급 통계/수식 계산 엔진의 서버 사이드 실시간 평가

  • 수만~수십만 행의 데이터를 처리할 때 발생하는 JVM OutOfMemoryError(OOM) 및 GC 스파이크

이 글에서는 Apache POI 환경에서 겪었던 실제 한계점들을 짚어보고, **DsExcel(Document Solutions for Excel, 구 GcExcel)**을 도입하여 아키텍처를 혁신한 경험을 공유합니다. 특히 프론트엔드의 SpreadJS와의 네이티브 JSON 연동 시너지, 그리고 과거 도메인 모델링 및 데이터 설계 과정에서 치열하게 고민했던 트리 구조 템플릿 임포트, 다중 헤더 그리드 내보내기, 복잡 수식 처리 전략을 코드 및 아키텍처 레벨에서 상세히 다룹니다.

2. Backend 엑셀 엔진 심층 비교: Apache POI vs DsExcel

백엔드에서 엑셀 라이브러리를 선택할 때 고려해야 하는 핵심 축은 메모리 아키텍처, 수식 계산 엔진, 템플릿 생산성, 그리고 라이선스 비용입니다.

2.1 아키텍처 및 메모리 관리 모델

Apache POI의 이중성 (XSSFWorkbook vs SXSSFWorkbook)

  • XSSFWorkbook (DOM 모델): 엑셀 파일(XLSX)의 모든 시트, 행, 셀, 스타일 객체를 JVM 힙 메모리에 완전한 객체 그래프로 로드합니다. 데이터가 수만 건을 넘어가면 셀 하나당 수많은 Java Wrapper 객체가 생성되어 메모리가 기하급수적으로 증가하고 곧바로 OOM이 발생합니다.

  • SXSSFWorkbook (스트리밍 윈도우 모델): OOM을 극복하기 위해 메모리에 슬라이딩 윈도우(예: 최근 100행)만 유지하고 나머지는 디스크 임시 파일에 플러시하는 방식입니다. 하지만 심각한 제약사항이 따릅니다.

  • 지나간 행에 대한 수정이나 역방향 접근이 불가능합니다.

  • 셀 병합(Merged Region)이나 계층형 헤더를 동적으로 재계산하기 매우 어렵습니다.

  • 시트 간 상호 참조 수식 계산이나 피벗 테이블 지원이 불가능에 가깝습니다.

  • 템플릿 기반 리포트 생성 시, 기존 템플릿의 서식을 읽으면서 스트리밍으로 쓰기 작업(Read-Write Template)을 병행하기 어렵습니다.

DsExcel의 고성능 엔진 아키텍처

  • DsExcel은 Microsoft Excel의 계산 엔진과 내부 데이터 구조를 완벽하게 모방하면서도, 자바/닷넷 환경에 맞게 고도로 최적화된 메모리 매핑 구조를 가집니다.

  • 포인터 및 원시 배열 기반의 메모리 경량화: POI처럼 셀마다 무거운 인스턴스를 무한정 띄우지 않고, 데이터와 스타일을 내부 인덱스 기반으로 캐싱하여 힙 점유율을 POI 대비 1/5 ~ 1/10 수준으로 억제합니다.

  • 임시 디스크 I/O 없이도 수십만 행 이상의 대용량 워크북을 순수 인메모리에서 빠른 속도로 생성/파싱할 수 있습니다.

2.2 수식 평가 엔진 (Formula Calculation Engine)의 차이

실무에서 자주 요청되는 SUMPRODUCT, DSUM, INDEX/MATCH, XLOOKUP, 그리고 동적 배열(Spill) 수식 처리는 엔진의 완성도를 가르는 결정적 척도입니다.

  • Apache POI: FormulaEvaluator가 내장되어 있으나 지원하는 함수 목록이 제한적입니다. 특히 다중 조건 배열 연산을 수행하는 SUMPRODUCT나 데이터베이스 함수 계열(DSUM, DCOUNT)을 평가할 때 구현체가 누락되어 있거나, 복잡한 중첩 괄호 파싱에서 NotImplementedException을 던지는 경우가 빈번합니다.

  • DsExcel: Microsoft Excel 사양을 100% 호환하는 독자적인 고성능 수식 연산 엔진을 내장하고 있어, 450개 이상의 엑셀 표준 함수를 오프라인(서버 사이드)에서 완벽히 계산합니다. Excel Desktop이나 오피스 인터롭(Interop) 설치 없이도 완벽한 수식 정합성을 보장합니다.

2.3 템플릿 바인딩 생산성

  • POI 방식: 디자인된 엑셀 서식을 불러온 뒤, 개발자가 일일이 행/열 인덱스를 카운팅(rowNum++, cellNum++)하고 스타일 폰트, 테두리, 배경색을 Java 코드로 주입해야 합니다. 기획자나 디자이너가 서식을 1줄만 수정해도 백엔드 인덱스 계산 로직 전체가 깨집니다.

  • DsExcel 방식: 강력한 Declarative Template Engine을 기본 제공합니다. 엑셀 파일 자체에 {{ds.fieldName}}, {{ds.group(R=T)}}와 같은 문법을 작성해두면, 백엔드에서는 단순히 Java POJO나 JSON 데이터셋을 넘겨주는 것만으로 반복 행 확장, 소계/합계 삽입, 동적 셀 병합이 자동으로 완료됩니다.

2.4 종합 비교 매트릭스

비교 항목

Apache POI (XSSF / SXSSF)

DsExcel (Document Solutions for Excel)

라이선스

오픈소스 (Apache License 2.0, 무료)

상용 라이선스 (MESCIUS, 개발자/코어 라이선스)

메모리 점유율

XSSF: 매우 높음 / SXSSF: 낮음

인메모리 최적화로 매우 낮음 및 안정적

스트리밍 제약

읽기/수정 제약 및 수식 계산 불가

풀 인터페이스 지원 (랜덤 액세스 가능)

SpreadJS 연동

비표준 (XLSX 바이트 파싱 필요)

네이티브 지원 (fromJson, toJson) 완벽 호환

수식 연산 지원

기본 함수 위주, 고급 수식 한계

450+ Excel 전 함수 및 동적 배열 지원

템플릿 엔진

없음 (수동 코딩 필요)

선언형 템플릿 문법 자체 내장

문서 변환 기능

별도 도구 결합 필요

PDF, HTML, 이미지 직접 변환 지원

3. Front-End(SpreadJS)와의 시너지: 완벽한 풀스택 엑셀 파이프라인

프론트엔드 그리드 컴포넌트로 MESCIUS의 SpreadJS를 채택한 시스템이라면, 백엔드 엔진으로 DsExcel을 선택하는 것은 단순한 대안이 아니라 필수적인 설계입니다.

3.1 무손실 네이티브 JSON 교환 (Zero-Fidelity Loss)

일반적으로 웹 화면(그리드)의 내용을 서버 엑셀로 내보내거나 가져오려면 다음과 같은 복잡한 병목 단계를 거칩니다.

  • 기존 방식 (SpreadJS + Apache POI):

a. 클라이언트(SpreadJS)에서 XLSX 바이너리 blob을 생성

b. 네트워크를 통해 수 MB의 바이너리 전송

c. 백엔드에서 POI가 ZIP 압축을 풀고 수천 개의 XML 파일을 파싱

d. 수정 후 다시 ZIP으로 압축하여 클라이언트로 반환

-문제점: CPU 과부하, 전송 대역폭 낭비, 그리고 POI가 해석하지 못하는 고급 CSS/테마/조건부 서식 소실 발생.

  • 모던 아키텍처 (SpreadJS + DsExcel):

-SpreadJS와 DsExcel은 **동일한 데이터 스키마와 JSON 사양(SpreadJS JSON Format)**을 공유합니다.

-클라이언트는 단지 그리드의 직렬화된 JSON 문자열(spread.toJSON())만 전송합니다.

-백엔드(DsExcel)는 단 한 줄로 이를 읽어 들입니다.

Workbook workbook = new Workbook();
workbook.fromJson(spreadJsonString);

-서버에서 비즈니스 로직 연산, 대량 데이터 병합, 마스킹 처리를 수행한 후, 다시 workbook.toJson()으로 내려주거나 최종 배포용 .xlsx 또는 .pdf 파일로 즉시 렌더링합니다.

3.2 스타일, 다중 병합, 조건부 서식의 무손실 보존

SpreadJS에서 사용자가 설정한 다중 헤더 스팬(Span), 폰트 계층, 데이터 바(Data Bar), 색상 스케일, 드롭다운 유효성 검사 규칙이 백엔드 DsExcel에 100% 동일하게 반영됩니다. POI 사용 시 일일이 CellStyle을 매핑하고 인덱스 오버플로우를 걱정해야 했던 구조적 문제가 완전히 소멸합니다.

4. 실전 사례 연구: 복잡한 엔터프라이즈 도메인 이슈 해결기

4.1 케이스 1: 다중 헤더(Multi-Header) 계층형 웹 그리드의 무손실 엑셀 입출력

  • 문제 정의: 화면의 웹 그리드가 3단계 이상의 그룹화된 헤더 구조를 가집니다. 이를 POI로 처리하려면 CellRangeAddress를 이용해 가로/세로 병합 좌표를 재귀적으로 계산해야 했으며, 컬럼이 동적으로 변경되면 좌표가 뒤틀렸습니다.

  • DsExcel + SpreadJS 해결책: SpreadJS의 Column Header 구조를 그대로 fromJson으로 DsExcel에 밀어 넣으면 내부 병합 좌표가 완벽하게 유지됩니다. 서버와 프론트 간의 헤더 스키마 불일치가 해소되었습니다.

4.2 케이스 2: 트리 구조(Tree-structure) 그룹-필드 데이터 임포트 템플릿

  • 문제 정의: 대분류 아래 중분류가 있고, 그 아래 동적인 속성 필드가 트리 구조로 묶인 엔티티 모델이었습니다.

  • DsExcel 템플릿 엔진 적용: 템플릿 XLSX에 {{ds.groupName(M=T)}}와 같은 선언형 마커를 설정하여 데이터셋을 넘겨주는 것만으로 동적 행 확장과 셀 병합을 자동화했습니다. 수동 포인터 계산 로직이 제거되어 유지보수성이 극대화되었습니다.

4.3 케이스 3: 고난도 수식(SUMPRODUCT, DSUM) 처리 및 서버 사이드 연산

  • 문제 정의: 업로드된 파일의 복잡한 조건부 가중치 계산 결과를 서버에서 즉각 검증해야 했습니다.

  • 해결 전략: DsExcel의 workbook.calculate()를 호출하여 내장된 Excel 호환 엔진이 모든 종속 셀을 파악하고 정확한 결괏값을 도출하게 함으로써, 자바 코드로 수학적 로직을 재구현하는 이중 작업을 방지했습니다.

4.4 케이스 4: 첨부 파일 및 데이터 관리 도메인 모델 아키텍처

  • 아키텍처 설계: 대용량 처리 시 HTTP 스레드 고갈을 막기 위해 비동기 큐 파이프라인(Spring Task / RabbitMQ)을 도입하고, try-with-resources를 통해 DsExcel의 워크북 자원을 즉각 해제하여 JVM 메모리를 안정적으로 관리했습니다.

5. 실전 코드 워크스루: POI vs DsExcel

5.1 Apache POI 구현 예시 (수동 좌표 매핑)

Workbook poiWorkbook = new XSSFWorkbook();
Sheet sheet = poiWorkbook.createSheet("계층형데이터");
Row headerRow1 = sheet.createRow(0);
headerRow1.createCell(0).setCellValue("조직 인프라 정보");
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 1));
int rowIndex = 2;
for (DepartmentDto dto : dataList) {
    Row row = sheet.createRow(rowIndex++);
    row.createCell(0).setCellValue(dto.getDivision());
    // ... 인덱스 수동 관리 및 스타일 설정 생략
}

5.2 DsExcel + SpreadJS 파이프라인 구현 예시

public byte[] generateExcelWithDsExcel(String spreadJsJson, List<DepartmentDto> dataList) {
    try (Workbook workbook = new Workbook()) {
        workbook.fromJson(spreadJsJson); // 프론트 디자인 로드
        IWorksheet sheet = workbook.getActiveSheet();
        sheet.addDataSource("dept", dataList); // 데이터 바인딩
        workbook.calculate(); // 수식 재계산

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        workbook.save(out, SaveFileFormat.Xlsx);
        return out.toByteArray();
    }
}

6. 결론 및 도입 의사결정 가이드

6.1 POI를 유지해도 좋은 경우

  • 단순 1차원 테이블 형태의 원시 데이터 익스포트

  • 고정된 양식의 소규모 엑셀 파일 생성 (수천 건 이하)

  • 추가 라이선스 비용 투자가 불가능한 오픈소스 프로젝트

6.2 DsExcel로의 전환을 강력히 권장하는 경우

  • SpreadJS를 도입하여 완벽한 WYSIWYG 환경이 필요한 프로젝트

  • 복잡한 다중 헤더, 트리형 데이터, 병합 셀이 다수 포함된 템플릿 관리

  • SUMPRODUCT, XLOOKUP 등 고난도 수식의 서버 사이드 신뢰성 평가 필요

  • POI의 OOM 장애로 인해 대용량 처리 시 시스템 안정성이 위협받는 경우

맺음말

현대 엔터프라이즈 아키텍처에서 엑셀은 단순한 파일이 아니라 핵심 비즈니스 규칙의 집합체입니다. DsExcel과 SpreadJS의 결합은 개발자에게 좌표 계산의 노가다에서 벗어나 비즈니스 로직에 집중할 수 있는 자유를 제공할 것입니다.

BigJumbo

Site footer