Backend/Java

Apache POI로 엑셀 차트 만들기 — Bar·Line 차트

Lior 2026. 5. 24. 13:54
Spring Boot × Apache POI 실무 시리즈 05/6

Apache POI로 엑셀 차트 만들기 — Bar·Line 차트

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

Apache POI 실무 시리즈

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

 

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

 

 

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

1. 소개

POI로 차트를 만드는 건 솔직히 좀 번거롭습니다. 스타일이나 데이터 쓰기보다 훨씬 많은 객체를 다뤄야 하고, 차트 관련 API가 굉장히 저수준이라 처음 보면 어디서부터 시작해야 할지 막막하기도 해요.

그래도 구조를 한 번 파악하고 나면 반복 패턴이 있어서 익숙해집니다. 이번 편에서는 바(Bar) 차트와 꺾은선(Line) 차트를 중심으로 차트 삽입의 전체 흐름을 잡아드릴게요.

차트 관련 핵심 클래스 구조

XSSFChart              → 차트 객체 자체
  XSSFChartLegend      → 범례
  XDDFCategoryAxis     → 카테고리(X) 축
  XDDFValueAxis        → 값(Y) 축
  XDDFDataSource       → 데이터 범위 (XDDFDataSourcesFactory)
  XDDFBarChartData     → 바 차트 데이터
  XDDFLineChartData    → 꺾은선 차트 데이터

차트를 만들 때는 이 객체들을 조립하는 흐름입니다.


2. 기본 개념

차트 생성 전체 흐름

POI에서 차트를 만드는 순서는 항상 이렇습니다.

1. 데이터를 시트에 먼저 씁니다
2. 차트를 그릴 영역(Drawing)을 잡습니다
3. 앵커(차트 위치/크기)를 설정합니다
4. 차트 객체를 생성합니다
5. 축(카테고리축, 값축)을 설정합니다
6. 데이터 시리즈를 연결합니다
7. 차트를 그립니다

뭔가 많아 보이지만, 3~4번까지는 거의 똑같이 쓰이는 코드라 템플릿화해두면 됩니다.


3. 바(Bar) 차트 만들기

데이터 준비

차트는 시트의 데이터를 참조하는 방식입니다. 먼저 시트에 데이터를 쓰고, 그 셀 범위를 차트에 연결합니다.

// 시트에 데이터를 먼저 씁니다
Sheet sheet = workbook.createSheet("차트");

// 헤더
Row header = sheet.createRow(0);
header.createCell(0).setCellValue("월");
header.createCell(1).setCellValue("매출");

// 데이터
String[] months = {"1월", "2월", "3월", "4월", "5월", "6월"};
int[] sales    = {1200000, 1500000, 1100000, 1800000, 2100000, 1700000};

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

바 차트 생성 전체 코드

public void createBarChart(Sheet sheet, int dataRows) {

    XSSFSheet xssfSheet = (XSSFSheet) sheet;

    // 1. Drawing 영역 — 차트가 그려지는 캔버스
    XSSFDrawing drawing = xssfSheet.createDrawingPatriarch();

    // 2. 앵커 — 차트의 위치와 크기 지정
    // createAnchor(dx1, dy1, dx2, dy2, col1, row1, col2, row2)
    // col, row는 셀 기준 좌표
    XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, 3, 0, 12, 20);

    // 3. 차트 객체 생성
    XSSFChart chart = drawing.createChart(anchor);
    chart.setTitleText("월별 매출");
    chart.setTitleOverlay(false);

    // 4. 범례 설정
    XDDFChartLegend legend = chart.getOrAddLegend();
    legend.setPosition(LegendPosition.BOTTOM);

    // 5. 축 설정 (XDDF: chart에서 직접 생성)
    XDDFCategoryAxis bottomAxis = chart.createCategoryAxis(AxisPosition.BOTTOM);
    XDDFValueAxis leftAxis = chart.createValueAxis(AxisPosition.LEFT);
    leftAxis.setCrosses(AxisCrosses.AUTO_ZERO);

    // 6. 데이터 소스 — 셀 범위 참조
    XDDFDataSource<String> categories = XDDFDataSourcesFactory.fromStringCellRange(
        xssfSheet, new CellRangeAddress(1, dataRows, 0, 0)); // 월

    XDDFNumericalDataSource<Double> values = XDDFDataSourcesFactory.fromNumericCellRange(
        xssfSheet, new CellRangeAddress(1, dataRows, 1, 1)); // 매출

    // 7. 차트 데이터 생성 (XDDF: createData + ChartTypes)
    XDDFBarChartData data = (XDDFBarChartData) chart.createData(
        ChartTypes.BAR, bottomAxis, leftAxis);
    data.setBarDirection(BarDirection.COL); // 세로 막대 (Column)

    // 8. 시리즈 추가
    XDDFBarChartData.Series series =
        (XDDFBarChartData.Series) data.addSeries(categories, values);
    series.setTitle("매출", null);

    // 9. 차트 그리기
    chart.plot(data);
}

가로 / 세로 방향 전환

POI의 바 차트 기본값이 가로 막대(Horizontal)라서, 우리가 흔히 쓰는 세로 막대(Column)로 만들려면 방향을 명시해야 합니다.

XDDFBarChartData data = (XDDFBarChartData) chart.createData(
    ChartTypes.BAR, bottomAxis, leftAxis);

// 세로 막대 (Column Chart)
data.setBarDirection(BarDirection.COL);

// 가로 막대 (Bar Chart)
data.setBarDirection(BarDirection.BAR);

전체 사용 예

@Component
public class BarChartExcelWriter {

    public Workbook createSalesReport() {
        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet("월별매출");

        // 데이터 작성
        writeData(sheet);

        // 차트 삽입 (6행의 데이터)
        createBarChart(sheet, 6);

        return workbook;
    }

    private void writeData(Sheet sheet) {
        Row header = sheet.createRow(0);
        header.createCell(0).setCellValue("월");
        header.createCell(1).setCellValue("매출");

        String[] months = {"1월", "2월", "3월", "4월", "5월", "6월"};
        int[] sales = {1200000, 1500000, 1100000, 1800000, 2100000, 1700000};

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

    public void createBarChart(Sheet sheet, int dataRows) {
        XSSFSheet xssfSheet = (XSSFSheet) sheet;
        XSSFDrawing drawing = xssfSheet.createDrawingPatriarch();
        XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, 3, 0, 12, 20);

        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(
            xssfSheet, new CellRangeAddress(1, dataRows, 0, 0));
        XDDFNumericalDataSource<Double> values = XDDFDataSourcesFactory.fromNumericCellRange(
            xssfSheet, new CellRangeAddress(1, dataRows, 1, 1));

        XDDFBarChartData data = (XDDFBarChartData) chart.createData(
            ChartTypes.BAR, bottomAxis, leftAxis);
        data.setBarDirection(BarDirection.COL);

        XDDFBarChartData.Series series =
            (XDDFBarChartData.Series) data.addSeries(categories, values);
        series.setTitle("매출", null);

        chart.plot(data);
    }
}

4. 꺾은선(Line) 차트 만들기

바 차트 구조를 이해했다면 꺾은선 차트는 거의 동일합니다. XDDFBarChartDataXDDFLineChartData, BarChartSeriesLineChartSeries로 바뀌는 게 핵심이에요.

단일 시리즈 꺾은선 차트

private void createLineChart(Sheet sheet, int dataRows) {

    XSSFSheet xssfSheet = (XSSFSheet) sheet;
    XSSFDrawing drawing = xssfSheet.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(
        xssfSheet, new CellRangeAddress(1, dataRows, 0, 0));
    XDDFNumericalDataSource<Double> values = XDDFDataSourcesFactory.fromNumericCellRange(
        xssfSheet, new CellRangeAddress(1, dataRows, 1, 1));

    // BarChartData 대신 XDDFLineChartData
    XDDFLineChartData data = (XDDFLineChartData) chart.createData(
        ChartTypes.LINE, bottomAxis, leftAxis);

    XDDFLineChartData.Series series =
        (XDDFLineChartData.Series) data.addSeries(categories, values);
    series.setTitle("방문자", null);
    series.setSmooth(false);
    series.setMarkerStyle(MarkerStyle.NONE);

    chart.plot(data);
}

복수 시리즈 추가 (여러 선)

실무에서는 여러 항목을 비교하는 차트가 많죠. 시리즈를 여러 개 추가하면 됩니다.

// 데이터 예시
// 월  | PC방문자 | 모바일방문자
// 1월 | 5000    | 8000
// 2월 | 5500    | 9200

XDDFLineChartData data = (XDDFLineChartData) chart.createData(
    ChartTypes.LINE, bottomAxis, leftAxis);

XDDFDataSource<String> categories = XDDFDataSourcesFactory.fromStringCellRange(
    xssfSheet, new CellRangeAddress(1, dataRows, 0, 0)); // 월

// PC 방문자 시리즈 (B열)
XDDFNumericalDataSource<Double> pcValues = XDDFDataSourcesFactory.fromNumericCellRange(
    xssfSheet, new CellRangeAddress(1, dataRows, 1, 1));
XDDFLineChartData.Series pcSeries =
    (XDDFLineChartData.Series) data.addSeries(categories, pcValues);
pcSeries.setTitle("PC 방문자", null);
pcSeries.setSmooth(false);

// 모바일 방문자 시리즈 (C열)
XDDFNumericalDataSource<Double> mobileValues = XDDFDataSourcesFactory.fromNumericCellRange(
    xssfSheet, new CellRangeAddress(1, dataRows, 2, 2));
XDDFLineChartData.Series mobileSeries =
    (XDDFLineChartData.Series) data.addSeries(categories, mobileValues);
mobileSeries.setTitle("모바일 방문자", null);
mobileSeries.setSmooth(false);

chart.plot(data);

5. 앵커 좌표 이해하기

차트를 원하는 위치에 정확히 배치하려면 앵커 좌표 개념을 알아야 합니다.

XSSFClientAnchor anchor = drawing.createAnchor(
    dx1, dy1,   // 시작점의 셀 내 오프셋 (0~1023)
    dx2, dy2,   // 끝점의 셀 내 오프셋 (0~1023)
    col1, row1, // 시작 셀 (0-based)
    col2, row2  // 끝 셀 (0-based)
);

쉽게 말하면, col1, row1이 차트의 좌상단 셀이고 col2, row2가 우하단 셀입니다. dx, dy는 셀 안에서의 세밀한 오프셋인데, 보통 0으로 써도 됩니다.

// 예: D열(3) 0행부터 M열(12) 20행까지 차트 배치
drawing.createAnchor(0, 0, 0, 0, 3, 0, 12, 20);

데이터가 A열~B열에 있을 때 차트를 옆(D열~M열)에 배치하는 패턴을 많이 씁니다.


6. 주의사항

차트는 XSSFWorkbook에서만 됩니다

SXSSFWorkbook은 차트를 지원하지 않습니다. 차트가 필요하다면 XSSFWorkbook을 써야 해요. 대용량 데이터 + 차트가 같이 필요한 경우라면 데이터 시트는 SAX로 읽어서 일반 XSSFWorkbook에 채워 넣는 방식으로 우회해야 합니다.

데이터가 시트에 있어야 차트가 작동합니다

POI의 차트는 시트의 셀 범위를 참조합니다. 차트에 직접 데이터를 주입하는 방식이 아니라, 셀에 데이터가 있어야 엑셀을 열었을 때 차트가 제대로 보입니다.

// 직접 값을 넣는 방식 — POI 생성 시엔 보이지만
// 엑셀에서 열면 차트가 비어있을 수 있습니다 (비권장)
XDDFDataSourcesFactory.fromArray(new Double[]{1500000.0}); // 비권장

// 셀 범위를 참조하는 방식이 올바른 방법입니다 (권장)
XDDFNumericalDataSource<Double> values = XDDFDataSourcesFactory.fromNumericCellRange(
    sheet, new CellRangeAddress(1, 6, 1, 1)); // 권장

POI 차트 API는 저수준입니다

솔직히 말하면 POI의 차트 API는 직관적이지 않아요. 특히 세밀한 스타일 설정(선 색상, 마커 모양, 그리드 라인 등)은 CT 접두사가 붙은 저수준 XML 객체를 직접 건드려야 합니다.

// 예: 선 색상 변경 (저수준 API)
CTLineChart lineChart = chart.getCTChart().getPlotArea().getLineChartList().get(0);
CTLineSer ser = lineChart.getSerList().get(0);
CTShapeProperties spPr = ser.addNewSpPr();
CTSolidColorFillProperties fill = spPr.addNewLn().addNewSolidFill();
fill.addNewSrgbClr().setVal(new byte[]{(byte)0xFF, (byte)0x00, (byte)0x00}); // 빨간색

이 부분이 복잡하다면 6편에서 다루는 다른 접근 방식도 참고해보세요.


7. 성능 / 구조 팁

차트 생성 로직은 별도 클래스로 분리하세요

차트 코드는 길어지기 마련이라, ChartCreator 같은 별도 클래스로 분리해두면 관리가 훨씬 편합니다.

@Component
public class ChartCreator {

    public void insertBarChart(XSSFSheet sheet, String title,
                               CellRangeAddress catRange,
                               CellRangeAddress valRange,
                               int col1, int row1, int col2, int row2) {

        XSSFDrawing drawing = sheet.createDrawingPatriarch();
        XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, col1, row1, col2, row2);

        XSSFChart chart = drawing.createChart(anchor);
        chart.setTitleText(title);
        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, catRange);
        XDDFNumericalDataSource<Double> values =
            XDDFDataSourcesFactory.fromNumericCellRange(sheet, valRange);

        XDDFBarChartData data = (XDDFBarChartData) chart.createData(
            ChartTypes.BAR, bottomAxis, leftAxis);
        data.setBarDirection(BarDirection.COL);

        XDDFBarChartData.Series series =
            (XDDFBarChartData.Series) data.addSeries(categories, values);
        series.setTitle(title, null);

        chart.plot(data);
    }
}

템플릿 방식이 더 편할 수 있습니다

차트 스타일을 POI 코드로 세밀하게 제어하는 건 생각보다 많이 번거롭습니다. 실무에서는 미리 차트 포맷을 잡아둔 .xlsx 템플릿 파일을 열고, 데이터만 채워 넣는 방식이 오히려 빠를 때가 많아요.

// 템플릿 파일에 이미 차트가 있고, 데이터만 업데이트
try (InputStream is = getClass().getResourceAsStream("/templates/chart-template.xlsx");
     Workbook workbook = new XSSFWorkbook(is)) {

    Sheet sheet = workbook.getSheetAt(0);

    // 데이터만 덮어씁니다 — 차트는 자동으로 업데이트됩니다
    for (int i = 0; i < salesData.size(); i++) {
        Row row = sheet.getRow(i + 1);
        row.getCell(1).setCellValue(salesData.get(i));
    }

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

엑셀에서 직접 만든 예쁜 차트 스타일 그대로 내려줄 수 있어서 디자인 완성도가 훨씬 높아집니다.


8. 정리

이번 편 핵심 정리입니다.

  • 차트 생성 순서: 데이터 쓰기 → Drawing 영역 → 앵커 → 차트 객체 → 축 → 데이터 시리즈 → plot
  • 바 차트는 XDDFBarChartData, 꺾은선은 XDDFLineChartData를 쓰고 나머지 흐름은 동일합니다
  • 세로 막대 차트로 만들려면 STBarDir.COL을 명시해야 합니다
  • 여러 시리즈는 addSeries()를 여러 번 호출하면 됩니다
  • 차트는 XSSFWorkbook에서만 되고 SXSSFWorkbook은 안 됩니다
  • 세밀한 스타일 조정이 필요하면 템플릿 방식이 더 현실적입니다

다음 편은 그 외 차트 유형입니다. 파이(Pie), 도넛(Doughnut), 영역(Area), 분산형(Scatter) 차트와 함께, 차트 XML을 직접 다루는 방법도 정리합니다.


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