Jxls - 엑셀 템플릿 기반 동적 데이터 처리와 사용자 정의 커맨드 확장

김삼현·2022년 1월 10일
post-thumbnail

개요

Jxls는 Java에서 엑셀 문서를 동적으로 생성할 수 있도록 도와주는 라이브러리이다.

템플릿 기반의 접근 방식을 사용하며, 별도의 복잡한 코딩 없이 엑셀 데이터 생성, 반복 처리, 셀 병합 등의 기능을 효율적으로 구현할 수 있다.

이 글에서는 Jxls의 기본 사용 방법을 설명하고, 사용자 정의 커맨드(Custom Command)를 통한 확장 방법을 소개한다.


1. Jxls의 기본 사용 방법

Jxls의 주요 기능은 엑셀 템플릿을 기반으로 데이터를 처리하는 것이다.

템플릿에는 특수한 주석 기반의 문법을 사용하여 데이터 바인딩, 반복 처리 등을 정의할 수 있다.

템플릿 작성 예제

jxls-1

  • A:1 코멘트: 템플릿이 적용될 영역 정의
    • jx:area(lastCell="E5"): 템플릿이 A1부터 E5까지 적용됨을 지정.
  • A:4 코멘트: 반복 처리 설정
    • jx:each(items="employees" var="employee" lastCell="E4"): employees라는 배열을 반복하며, A4~E4 영역에 데이터를 출력.
  • A4: 테이블 내 EL 표현식: Jsp EL(Expression Language)와 유사한 문법으로 데이터를 출력
    • ${employee.name}: 반복 처리 중 현재 객체의 name 속성 값을 출력.

템플릿 처리 결과

템플릿을 사용하여 데이터를 바인딩한 결과는 다음과 같다.

formulas_output.png

위와 같이 데이터를 동적으로 생성할 수 있다.


2. 사용자 정의 커맨드를 활용한 확장

Jxls는 기본적으로 제공하는 jx:each, jx:area 등의 커맨드를 통해 데이터 바인딩 및 반복 작업을 수행한다.

하지만 요구사항에 따라 기본 커맨드만으로는 충분하지 않을 수 있다.

이 경우, 사용자 정의 커맨드(Custom Command)를 작성해 Jxls의 기능을 확장할 수 있다.

확장 예제: 반복 처리와 셀 병합

요구사항:

회사-부서-직원 데이터를 엑셀로 출력하되,

  • 같은 부서에 속한 직원은 부서 이름 셀을 병합.
  • 같은 회사에 속한 부서는 회사 이름 셀을 병합.

결과 예시

jxls-2

이와 같은 구조를 구현하기 위해 사용자 정의 커맨드를 작성한다.


2.1 사용자 정의 커맨드: EachMergeCommand

EachMergeCommand는 Jxls의 기본 EachCommand를 상속하여,

반복 처리 중 자동으로 셀 병합을 수행하는 커맨드이다.

구현 코드


import org.jxls.area.Area;
import org.jxls.command.EachCommand;
import org.jxls.common.CellRef;
import org.jxls.common.Context;
import org.jxls.common.Size;

import java.util.List;
import java.util.stream.Collectors;

public class EachMergeCommand extends EachCommand {
    public static final String COMMAND_NAME = "each-merge";

    @Override
    public Size applyAt(CellRef cellRef, Context context) {
        // 하위 영역 수집
        List<Area> childAreas = this.getAreaList().stream()
                .flatMap(area -> area.getCommandDataList().stream())
                .flatMap(commandData -> commandData.getCommand().getAreaList().stream())
                .collect(Collectors.toList());

        // 병합 리스너 추가
        MergeAreaListener listener = new MergeAreaListener(this.getTransformer(), cellRef);
        this.getAreaList().get(0).addAreaListener(listener);
        childAreas.forEach(childArea -> childArea.addAreaListener(listener));

        // EachCommand의 기본 처리 수행
        return super.applyAt(cellRef, context);
    }
}

2.2 셀 병합 리스너: MergeAreaListener

셀 병합 작업은 AreaListener를 통해 구현할 수 있다.

이 클래스는 각 셀에 대한 작업이 완료될 때 호출되며, 이를 활용해 병합 처리를 수행한다.

구현 코드

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.jxls.common.AreaListener;
import org.jxls.common.CellRef;
import org.jxls.common.Context;
import org.jxls.transform.Transformer;
import org.jxls.transform.poi.PoiTransformer;

public class MergeAreaListener implements AreaListener {
    private final CellRef commandCell;
    private final Sheet sheet;
    private CellRef lastRowCellRef;

    public MergeAreaListener(Transformer transformer, CellRef cellRef) {
        this.commandCell = cellRef;
        this.sheet = ((PoiTransformer) transformer).getXSSFWorkbook().getSheet(cellRef.getSheetName());
    }

    @Override
    public void afterApplyAtCell(CellRef cellRef, Context context) {
        if (commandCell.getCol() != cellRef.getCol()) {
            this.setLastRowCellRef(cellRef);
        } else {
            if (!existMerged(cellRef)) merge(cellRef);
        }
    }

    private void merge(CellRef cellRef) {
        if (lastRowCellRef == null) return;
        int from = cellRef.getRow();
        int to = lastRowCellRef.getRow();
        sheet.addMergedRegion(new CellRangeAddress(from, to, cellRef.getCol(), cellRef.getCol()));
    }

    private void setLastRowCellRef(CellRef cellRef) {
        if (lastRowCellRef == null || lastRowCellRef.getRow() < cellRef.getRow()) {
            this.lastRowCellRef = cellRef;
        }
    }

    private boolean existMerged(CellRef cell) {
        return sheet.getMergedRegions().stream().anyMatch(address -> address.isInRange(cell.getRow(), cell.getCol()));
    }
}

2.3 커맨드 등록 및 사용

사용자 정의 커맨드를 Jxls에 등록하려면 다음과 같이 addCommandMapping 메서드를 호출한다.


XlsCommentAreaBuilder.addCommandMapping(EachMergeCommand.COMMAND_NAME, EachMergeCommand.class);

2.4 실제 사용 예제

ExcelGenerator 클래스를 작성하여 엑셀 생성 과정을 간소화한다.

ExcelGenerator.java

import org.jxls.builder.xls.XlsCommentAreaBuilder;
import org.jxls.common.Context;
import org.jxls.util.JxlsHelper;

import java.io.*;

public class ExcelGenerator {
    private final String templatePath;
    private final Context context;

    public ExcelGenerator(String templatePath) {
        this.templatePath = templatePath;
        this.context = new Context();
        XlsCommentAreaBuilder.addCommandMapping(EachMergeCommand.COMMAND_NAME, EachMergeCommand.class);
    }

    public void addMappingValue(String varName, Object value) {
        this.context.putVar(varName, value);
    }

    public void generate(String outputPath) {
        try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(templatePath);
             OutputStream os = new FileOutputStream(outputPath)) {
            JxlsHelper.getInstance().processTemplate(is, os, this.context);
        } catch (IOException e) {
            throw new RuntimeException("Template processing error", e);
        }
    }
}

테스트 코드

@Test
void nestedEachMergeGenerateTest() {
    ExcelGenerator generator = new ExcelGenerator("nested-each-merge-template.xlsx");
    generator.addMappingValue("companies", mockCompanyData());
    Assertions.assertDoesNotThrow(() -> generator.generate("output.xlsx"));
}

출력 결과

템플릿:

nested-each-merge-template.png

결과:

nested-each-merge-output.png


3. 결론

Jxls는 템플릿 기반 접근 방식을 통해 엑셀 문서 생성을 단순화하며, 사용자 정의 커맨드를 통해 확장성을 제공한다.

반복 처리, 셀 병합 같은 고급 기능을 효율적으로 구현할 수 있어 데이터 보고서 생성 등에 적합하다.

참고 자료

0개의 댓글