Backend/Java

Apache POI 차트 고급편 — Pie·Doughnut·Area·Scatter·OOXML 제어

Lior 2026. 5. 25. 16:49
Spring Boot × Apache POI 실무 시리즈 06/6

Apache POI 차트 고급편 — Pie·Doughnut·Area·Scatter·OOXML 제어

실무에서 자주 만나는 Excel 처리 요구사항을 Spring Boot와 Apache POI 기준으로 정리한 시리즈입니다.

Apache POI 실무 시리즈

  1.  엑셀 읽기
  2.  엑셀 쓰기
  3.  대용량 엑셀 읽기
  4.  대용량 엑셀 쓰기
  5.  차트 Bar/Line
  6.  기타 차트/XML 제어 ← 현재 글

 

이 글에서 다루는 내용
기타 차트/XML 제어를 Spring Boot 프로젝트에서 실무적으로 적용하는 방법, 자주 발생하는 함정, 구조화 패턴을 함께 정리합니다.

 

 

Apache POI 차트 고급편 — Pie·Doughnut·Area·Scatter·OOXML 제어 흐름 요약
이 글은 예제 코드보다 실무에서 놓치기 쉬운 포인트를 중심으로 정리했습니다. 필요한 코드는 그대로 복사해서 프로젝트 구조에 맞게 분리해 사용하면 됩니다.

1. 소개

시리즈 마지막 편입니다.

5편에서 바 차트와 꺾은선 차트의 기본 흐름을 잡았으니, 이번엔 파이(Pie), 도넛(Doughnut), 영역(Area), 분산형(Scatter) 차트를 다룹니다. 그리고 POI의 고수준 API로는 표현이 안 되는 부분을 OOXML XML을 직접 제어해서 처리하는 방법도 정리할게요.

솔직히 이 편에서 다루는 내용은 POI 차트 API의 가장 어두운 부분입니다. API가 일관성이 없고, 차트 유형마다 지원 수준이 달라서 “왜 이게 안 되지?” 하는 순간이 분명 옵니다. 그 부분까지 같이 짚어드릴게요.


2. 파이(Pie) 차트

파이 차트는 비율을 보여줄 때 쓰는 가장 흔한 유형 중 하나입니다.

데이터 준비

Sheet sheet = workbook.createSheet("파이차트");

// 헤더
Row header = sheet.createRow(0);
header.createCell(0).setCellValue("카테고리");
header.createCell(1).setCellValue("비율");

// 데이터
String[] categories = {"의류", "전자", "식품", "가구", "기타"};
double[] values     = {35.0, 25.0, 20.0, 12.0, 8.0};

for (int i = 0; i < categories.length; i++) {
    Row row = sheet.createRow(i + 1);
    row.createCell(0).setCellValue(categories[i]);
    row.createCell(1).setCellValue(values[i]);
}

파이 차트 생성

private void createPieChart(XSSFSheet sheet, int dataRows) {

    XSSFDrawing drawing = sheet.createDrawingPatriarch();
    XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, 3, 0, 13, 20);

    XSSFChart chart = drawing.createChart(anchor);
    chart.setTitleText("카테고리별 매출 비율");
    chart.setTitleOverlay(false);

    XDDFChartLegend legend = chart.getOrAddLegend();
    legend.setPosition(LegendPosition.RIGHT);

    // 파이 차트는 축이 없으므로 null 전달
    XDDFDataSource<String> categories = XDDFDataSourcesFactory.fromStringCellRange(
        sheet, new CellRangeAddress(1, dataRows, 0, 0));
    XDDFNumericalDataSource<Double> values = XDDFDataSourcesFactory.fromNumericCellRange(
        sheet, new CellRangeAddress(1, dataRows, 1, 1));

    XDDFPieChartData data = (XDDFPieChartData) chart.createData(
        ChartTypes.PIE, null, null);
    data.setVaryColors(true);

    XDDFPieChartData.Series series =
        (XDDFPieChartData.Series) data.addSeries(categories, values);
    series.setTitle("매출 비율", null);
    series.setExplosion(0L);

    chart.plot(data);
}

바/꺾은선과 비교했을 때 가장 큰 차이가 축이 없다는 점이에요. chart.plot(data)에 축을 안 넘기면 됩니다.

조각 분리(Explosion) 효과

특정 조각을 강조하고 싶을 때 분리 효과를 줄 수 있습니다.

XDDFPieChartData.Series series =
    (XDDFPieChartData.Series) data.addSeries(categories, values);
series.setExplosion(10L); // 10% 분리

단, 이 설정은 모든 조각에 일괄 적용됩니다. 특정 조각만 분리하려면 저수준 XML을 건드려야 합니다.


3. 도넛(Doughnut) 차트

도넛 차트는 파이 차트의 변형으로, 가운데가 뚫려 있는 형태입니다. POI의 고수준 API에 DoughnutChartData가 없기 때문에, XML을 직접 다뤄야 합니다.

// 도넛 차트는 XDDF에 아직 고수준 API가 없어
// CT* 클래스로 직접 XML을 구성합니다
private void createDoughnutChart(XSSFSheet sheet, int dataRows) {

    XSSFDrawing drawing = sheet.createDrawingPatriarch();
    XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, 3, 0, 13, 20);
    XSSFChart chart = drawing.createChart(anchor);

    chart.setTitleText("도넛 차트");
    chart.setTitleOverlay(false);

    XDDFChartLegend legend = chart.getOrAddLegend();
    legend.setPosition(LegendPosition.RIGHT);

    CTPlotArea plotArea = chart.getCTChart().getPlotArea();
    CTDoughnutChart doughnutChart = plotArea.addNewDoughnutChart();

    CTDPtSer ser = doughnutChart.addNewSer();
    ser.addNewIdx().setVal(0);
    ser.addNewOrder().setVal(0);

    CTSerTx tx = ser.addNewTx();
    tx.addNewV().setStringValue("매출 비율");

    CTStrRef catStrRef = ser.addNewCat().addNewStrRef();
    catStrRef.setF("'도넛차트'!$A$2:$A$6");

    CTNumRef valNumRef = ser.addNewVal().addNewNumRef();
    valNumRef.setF("'도넛차트'!$B$2:$B$6");

    CTHoleSize holeSize = doughnutChart.addNewHoleSize();
    holeSize.setVal((short) 50);

    doughnutChart.addNewDLbls().addNewShowVal().setVal(true);
}

도넛 차트는 이렇게 XML을 직접 만들어야 합니다. 번거롭지만 한 번 패턴을 잡아두면 재사용할 수 있어요.


4. 영역(Area) 차트

꺾은선 차트에서 선 아래 영역이 채워지는 형태입니다. 추세를 보여줄 때 꺾은선보다 시각적으로 더 강조되는 느낌을 줍니다.

private void createAreaChart(XSSFSheet sheet, int dataRows) {

    XSSFDrawing drawing = sheet.createDrawingPatriarch();
    XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, 3, 0, 14, 22);

    XSSFChart chart = drawing.createChart(anchor);
    chart.setTitleText("누적 방문자");
    chart.setTitleOverlay(false);

    XDDFChartLegend legend = chart.getOrAddLegend();
    legend.setPosition(LegendPosition.BOTTOM);

    XDDFCategoryAxis bottomAxis = chart.createCategoryAxis(AxisPosition.BOTTOM);
    XDDFValueAxis leftAxis = chart.createValueAxis(AxisPosition.LEFT);
    leftAxis.setCrosses(AxisCrosses.AUTO_ZERO);

    XDDFDataSource<String> categories = XDDFDataSourcesFactory.fromStringCellRange(
        sheet, new CellRangeAddress(1, dataRows, 0, 0));
    XDDFNumericalDataSource<Double> values = XDDFDataSourcesFactory.fromNumericCellRange(
        sheet, new CellRangeAddress(1, dataRows, 1, 1));

    XDDFAreaChartData data = (XDDFAreaChartData) chart.createData(
        ChartTypes.AREA, bottomAxis, leftAxis);

    XDDFAreaChartData.Series series =
        (XDDFAreaChartData.Series) data.addSeries(categories, values);
    series.setTitle("방문자", null);

    chart.plot(data);
}

바 차트나 꺾은선 차트랑 구조가 거의 동일해서 가장 쉬운 편입니다. XDDFAreaChartData로만 바꿔주면 돼요.


5. 분산형(Scatter) 차트

두 값 사이의 상관관계를 보여줄 때 씁니다. 카테고리 없이 X, Y 모두 숫자를 씁니다.

// 분산형 데이터 예시 — X: 광고비, Y: 매출
// X    Y
// 100  800
// 200  1500
// 300  2100
// 400  3200
// 500  4100

private void createScatterChart(XSSFSheet sheet, int dataRows) {

    XSSFDrawing drawing = sheet.createDrawingPatriarch();
    XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, 3, 0, 14, 22);

    XSSFChart chart = drawing.createChart(anchor);
    chart.setTitleText("광고비 vs 매출");
    chart.setTitleOverlay(false);

    // 분산형은 양쪽 모두 XDDFValueAxis
    XDDFValueAxis bottomAxis = chart.createValueAxis(AxisPosition.BOTTOM);
    XDDFValueAxis leftAxis = chart.createValueAxis(AxisPosition.LEFT);
    leftAxis.setCrosses(AxisCrosses.AUTO_ZERO);

    XDDFNumericalDataSource<Double> xValues = XDDFDataSourcesFactory.fromNumericCellRange(
        sheet, new CellRangeAddress(1, dataRows, 0, 0));
    XDDFNumericalDataSource<Double> yValues = XDDFDataSourcesFactory.fromNumericCellRange(
        sheet, new CellRangeAddress(1, dataRows, 1, 1));

    XDDFScatterChartData data = (XDDFScatterChartData) chart.createData(
        ChartTypes.SCATTER, bottomAxis, leftAxis);
    data.setStyle(ScatterStyle.LINE_MARKER);

    XDDFScatterChartData.Series series =
        (XDDFScatterChartData.Series) data.addSeries(xValues, yValues);
    series.setTitle("광고비 vs 매출", null);
    series.setSmooth(false);

    chart.plot(data);
}

분산형에서 바 차트와 다른 점은 X축도 ValueAxis라는 거예요. 카테고리(문자열)가 아니라 숫자값이기 때문에 CategoryAxis 대신 ValueAxis를 씁니다.


6. 저수준 XML 직접 제어

POI 고수준 API로 안 되는 것들이 꽤 있습니다. 예를 들면:

  • 선 색상, 굵기 변경
  • 마커(점) 모양 변경
  • 데이터 레이블 위치 조정
  • 그리드 라인 스타일
  • 차트 배경색

이런 건 CT 접두사가 붙은 저수준 XML 객체를 직접 다뤄야 합니다.

꺾은선 색상 변경

// 차트를 만들고 plot()까지 호출한 다음에 저수준 접근
CTPlotArea plotArea = chart.getCTChart().getPlotArea();
CTLineChart lineChart = plotArea.getLineChartList().get(0);
CTLineSer series = lineChart.getSerList().get(0);

// 선 색상 설정 (빨간색: #FF0000)
CTShapeProperties spPr = series.addNewSpPr();
CTSolidColorFillProperties solidFill = spPr.addNewLn().addNewSolidFill();
solidFill.addNewSrgbClr().setVal(new byte[]{(byte)0xFF, 0x00, 0x00});

데이터 레이블 표시

CTLineChart lineChart = plotArea.getLineChartList().get(0);
CTDLbls dLbls = lineChart.addNewDLbls();
dLbls.addNewShowVal().setVal(true);       // 값 표시
dLbls.addNewShowLegendKey().setVal(false);
dLbls.addNewShowCatName().setVal(false);
dLbls.addNewShowSerName().setVal(false);
dLbls.addNewShowPercent().setVal(false);
dLbls.addNewShowBubbleSize().setVal(false);

마커(점) 없애기 — 선만 표시

CTLineSer ser = lineChart.getSerList().get(0);
CTMarker marker = ser.addNewMarker();
CTMarkerStyle markerStyle = marker.addNewSymbol();
markerStyle.setVal(STMarkerStyle.NONE); // 마커 없음

바 차트 색상 변경 (시리즈별)

CTPlotArea plotArea = chart.getCTChart().getPlotArea();
CTBarChart barChart = plotArea.getBarChartList().get(0);
CTBarSer series = barChart.getSerList().get(0);

CTShapeProperties spPr = series.addNewSpPr();
CTSolidColorFillProperties fill = spPr.addNewSolidFill();
fill.addNewSrgbClr().setVal(new byte[]{0x00, (byte)0x70, (byte)0xC0}); // 파란색

7. 차트 유형별 지원 상태 정리

POI 5.3.0 기준으로 각 차트 유형의 고수준 API 지원 상태입니다.

차트 유형 고수준 API 비고
바(Bar) ✅ 지원 BarChartData
꺾은선(Line) ✅ 지원 LineChartData
영역(Area) ✅ 지원 XDDFAreaChartData
파이(Pie) ✅ 지원 XDDFPieChartData
분산형(Scatter) ✅ 지원 XDDFScatterChartData
도넛(Doughnut) ❌ 미지원 XML 직접 제어 필요
거품형(Bubble) ❌ 미지원 XML 직접 제어 필요
방사형(Radar) ❌ 미지원 XML 직접 제어 필요
주식형(Stock) ❌ 미지원 XML 직접 제어 필요
혼합형(Combo) ❌ 미지원 XML 직접 제어 필요

미지원 유형이 꽤 많습니다. 도넛처럼 자주 쓰는 것도 직접 XML을 다뤄야 하는 상황이라, 복잡한 차트 요구사항이 있다면 다음 대안도 고려해보세요.


8. 복잡한 차트, 현실적인 대안들

대안 1: 템플릿 파일 활용

이 시리즈에서 여러 번 언급했는데, 차트 스타일이 복잡할수록 템플릿 방식이 훨씬 현실적입니다.

엑셀에서 원하는 차트를 완벽하게 만들어두고, 데이터 범위만 연결해두면 POI에서 데이터만 업데이트해도 차트가 자동으로 갱신됩니다.

// 템플릿에 이미 완성된 차트가 있고
// 데이터만 업데이트하면 차트가 자동 갱신
try (InputStream template = getClass().getResourceAsStream("/templates/chart.xlsx");
     XSSFWorkbook workbook = new XSSFWorkbook(template)) {

    XSSFSheet sheet = workbook.getSheetAt(0);

    for (int i = 0; i < data.size(); i++) {
        XSSFRow row = sheet.getRow(i + 1);
        if (row == null) row = sheet.createRow(i + 1);
        row.createCell(0).setCellValue(data.get(i).getCategory());
        row.createCell(1).setCellValue(data.get(i).getValue());
    }

    workbook.write(response.getOutputStream());
}

대안 2: ECharts/Chart.js로 이미지 생성 후 삽입

서버에서 차트를 PNG 이미지로 렌더링하고, 그 이미지를 엑셀에 삽입하는 방법입니다. 차트 자유도가 훨씬 높아요.

// 1. 이미지 생성 (외부 라이브러리 활용)
byte[] chartImageBytes = chartRenderer.renderAsPNG(chartConfig);

// 2. 엑셀에 이미지 삽입
int pictureIdx = workbook.addPicture(chartImageBytes, Workbook.PICTURE_TYPE_PNG);
CreationHelper helper = workbook.getCreationHelper();
ClientAnchor anchor = helper.createClientAnchor();
anchor.setCol1(3); anchor.setRow1(0);
anchor.setCol2(12); anchor.setRow2(20);

Drawing<?> drawing = sheet.createDrawingPatriarch();
drawing.createPicture(anchor, pictureIdx);

엑셀 내장 차트는 아니지만, 이미지라서 어떤 형태든 표현이 가능하다는 장점이 있습니다.

대안 3: JFreeChart

자바 환경에서 차트 이미지를 생성하는 라이브러리입니다. 차트 유형과 스타일 지원이 POI보다 훨씬 풍부해요.

// JFreeChart로 차트 생성 후 이미지로 변환
JFreeChart chart = ChartFactory.createPieChart(
    "카테고리별 비율", dataset, true, true, false);

// 이미지로 변환
byte[] imageBytes = ChartUtils.encodeAsPNG(chart.createBufferedImage(800, 600));

// POI로 엑셀에 삽입
int pictureIdx = workbook.addPicture(imageBytes, Workbook.PICTURE_TYPE_PNG);
// ... 앵커 설정 후 삽입

9. 주의사항

차트 만들고 나서 꼭 열어서 확인하세요

POI로 만든 차트는 코드에서 오류 없이 돌아도 엑셀을 실제로 열었을 때 깨지거나 비어있는 경우가 있습니다. 특히 저수준 XML을 건드렸을 때 이런 일이 생깁니다.

Excel에서 파일 열고 → 파일 → 정보 → 문제 확인을 눌러보면 어떤 부분이 손상됐는지 알 수 있어요.

엑셀 버전별로 차트 렌더링이 다를 수 있습니다

LibreOffice, Google Sheets에서 열면 MS Excel과 차트가 다르게 보이는 경우가 있습니다. 특히 저수준 XML로 스타일을 건드렸을 때 호환성 문제가 생기기 쉬워요. 최종 결과물을 MS Excel에서 꼭 확인해보세요.

POI 버전마다 차트 API가 조금씩 다릅니다

인터넷에서 찾은 POI 차트 예제 코드가 안 되는 경우, 버전이 다른 경우가 많아요. 이 시리즈는 5.3.0 기준이지만, 구버전(3.x, 4.x) 코드는 메서드 이름이나 패키지가 다를 수 있습니다.


10. 시리즈 전체 정리

6편에 걸쳐 다룬 내용을 한눈에 정리합니다.

핵심 내용 주요 클래스
1편 엑셀 읽기, 셀 타입 처리 XSSFWorkbook, WorkbookFactory
2편 엑셀 쓰기, 스타일/포맷/다운로드 CellStyle, DataFormat, SXSSFWorkbook
3편 대용량 읽기, 스트리밍 파싱 SheetContentsHandler, XSSFSheetXMLHandler
4편 대용량 쓰기, 슬라이딩 윈도우 SXSSFWorkbook, dispose()
5편 바/꺾은선 차트 XDDFBarChartData, XDDFLineChartData
6편 파이/분산형/영역 차트, XML 제어 XDDFPieChartData, CTPlotArea, CT*

Apache POI가 처음에는 진입 장벽이 있지만, 구조를 이해하고 나면 패턴이 반복됩니다. 이 시리즈가 실무에서 엑셀 기능을 구현할 때 도움이 됐으면 합니다.


시리즈 전체 목차
1. 엑셀 읽기
2. 엑셀 쓰기
3. 대용량 엑셀 읽기 (SAX / Event API)
4. 대용량 엑셀 쓰기 (SXSSFWorkbook)
5. 차트 삽입하기 — 바(Bar) · 꺾은선(Line)
6. 차트 삽입하기 — 그 외 차트 유형 ← 현재 글