Jxls는 Java에서 엑셀 문서를 동적으로 생성할 수 있도록 도와주는 라이브러리이다.
템플릿 기반의 접근 방식을 사용하며, 별도의 복잡한 코딩 없이 엑셀 데이터 생성, 반복 처리, 셀 병합 등의 기능을 효율적으로 구현할 수 있다.
이 글에서는 Jxls의 기본 사용 방법을 설명하고, 사용자 정의 커맨드(Custom Command)를 통한 확장 방법을 소개한다.
Jxls의 주요 기능은 엑셀 템플릿을 기반으로 데이터를 처리하는 것이다.
템플릿에는 특수한 주석 기반의 문법을 사용하여 데이터 바인딩, 반복 처리 등을 정의할 수 있다.

jx:area(lastCell="E5"): 템플릿이 A1부터 E5까지 적용됨을 지정.jx:each(items="employees" var="employee" lastCell="E4"): employees라는 배열을 반복하며, A4~E4 영역에 데이터를 출력.${employee.name}: 반복 처리 중 현재 객체의 name 속성 값을 출력.템플릿을 사용하여 데이터를 바인딩한 결과는 다음과 같다.

위와 같이 데이터를 동적으로 생성할 수 있다.
Jxls는 기본적으로 제공하는 jx:each, jx:area 등의 커맨드를 통해 데이터 바인딩 및 반복 작업을 수행한다.
하지만 요구사항에 따라 기본 커맨드만으로는 충분하지 않을 수 있다.
이 경우, 사용자 정의 커맨드(Custom Command)를 작성해 Jxls의 기능을 확장할 수 있다.
요구사항:
회사-부서-직원 데이터를 엑셀로 출력하되,

이와 같은 구조를 구현하기 위해 사용자 정의 커맨드를 작성한다.
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);
}
}
셀 병합 작업은 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()));
}
}
사용자 정의 커맨드를 Jxls에 등록하려면 다음과 같이 addCommandMapping 메서드를 호출한다.
XlsCommentAreaBuilder.addCommandMapping(EachMergeCommand.COMMAND_NAME, EachMergeCommand.class);
ExcelGenerator 클래스를 작성하여 엑셀 생성 과정을 간소화한다.
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"));
}
템플릿:

결과:

Jxls는 템플릿 기반 접근 방식을 통해 엑셀 문서 생성을 단순화하며, 사용자 정의 커맨드를 통해 확장성을 제공한다.
반복 처리, 셀 병합 같은 고급 기능을 효율적으로 구현할 수 있어 데이터 보고서 생성 등에 적합하다.