Apache POI로 엑셀 차트 만들기 — Bar·Line 차트
실무에서 자주 만나는 Excel 처리 요구사항을 Spring Boot와 Apache POI 기준으로 정리한 시리즈입니다.
Apache POI 실무 시리즈
- 엑셀 읽기
- 엑셀 쓰기
- 대용량 엑셀 읽기
- 대용량 엑셀 쓰기
- 차트 Bar/Line ← 현재 글
- 기타 차트/XML 제어
차트 삽입하기 Bar/Line를 Spring Boot 프로젝트에서 실무적으로 적용하는 방법, 자주 발생하는 함정, 구조화 패턴을 함께 정리합니다.

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) 차트 만들기
바 차트 구조를 이해했다면 꺾은선 차트는 거의 동일합니다. XDDFBarChartData → XDDFLineChartData, BarChartSeries → LineChartSeries로 바뀌는 게 핵심이에요.
단일 시리즈 꺾은선 차트
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. 차트 삽입하기 — 그 외 차트 유형