어노테이션을 이용한 고정 열 너비 설정
EasyExcel에서는 @ColumnWidth 어노테이션을 사용하여 특정 열의 너비를 고정값으로 지정할 수 있습니다. 하지만 이 방식은 런타임 시 동적으로 너비를 조절할 수 없으며, 코드를 수정해야만 값을 변경할 수 있다는 한계가 있습니다. 또한, 엑셀의 사양상 열 너비의 최댓값은 255로 제한되어 있어 이 값을 초과할 경우 예외가 발생합니다.
@Data
public class EmployeeRecord {
@ExcelProperty("직원 이름")
private String employeeName;
@ExcelProperty("입사일")
private LocalDate joinDate;
@ColumnWidth(40)
@ExcelProperty("연봉")
private BigDecimal salary;
}
스타일 전략(Style Strategy) 클래스 활용
더 유연한 열 너비 조정을 위해서는 EasyExcel이 제공하는 WriteHandler 인터페이스를 구현하거나, 기존 추상 클래스를 상속받아 커스텀 전략을 등록해야 합니다.
AbstractHeadColumnWidthStyleStrategy
이 추상 클래스는 헤더(Header) 행의 열 너비를 설정하는 데 특화되어 있습니다. columnWidth 메서드를 오버라이딩하여 각 열 인덱스에 따른 너비를 반환하도록 구현할 수 있습니다.
public abstract class AbstractHeadColumnWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {
@Override
protected void setColumnWidth(WriteSheetHolder sheetHolder, List> cellDataList,
Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
boolean shouldSetWidth = relativeRowIndex != null && (isHead || relativeRowIndex == 0);
if (!shouldSetWidth) {
return;
}
Integer computedWidth = columnWidth(head, cell.getColumnIndex());
if (computedWidth != null) {
// 엑셀 내부 너비 계산을 위해 256을 곱함
sheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), computedWidth * 256);
}
}
protected abstract Integer columnWidth(Head head, Integer columnIndex);
}
SimpleColumnWidthStyleStrategy
모든 열에 대해 동일한 고정 너비를 적용하고 싶을 때 사용하는 기본 제공 전략입니다.
@Data
public class CustomerProfile {
@ExcelProperty("고객 ID")
private Long customerId;
@ExcelProperty("이름")
private String fullName;
@ExcelProperty("연락처")
private String contactNumber;
@ExcelProperty("이메일 주소")
private String emailAddress;
@ExcelProperty("가입일")
private LocalDateTime registeredAt;
}
@GetMapping("/export/customers")
public void exportCustomers(HttpServletResponse httpResponse) throws IOException {
httpResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
httpResponse.setCharacterEncoding("UTF-8");
String encodedFileName = URLEncoder.encode("고객_목록", "UTF-8").replaceAll("\\+", "%20");
httpResponse.setHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".xlsx");
List<CustomerProfile> customers = customerService.fetchAllCustomers();
EasyExcel.write(httpResponse.getOutputStream(), CustomerProfile.class)
.sheet("고객 데이터")
.registerWriteHandler(new SimpleColumnWidthStyleStrategy(25))
.doWrite(customers);
}
LongestMatchColumnWidthStyleStrategy
데이터의 실제 길이에 맞춰 열 너비를 자동으로 조절하는 자동 맞춤 전략입니다. 헤더와 모든 데이터 셀을 순회하며 가장 긴 텍스트를 기준으로 너비를 설정합니다. 하지만 공식 문서에서도 언급되듯, 숫자나 특수 문자가 포함될 경우 줄바꿈이 발생하거나 실제 텍스트 길이와 완벽하게 일치하지 않는 등 다소 부정확한 면이 있어 정교한 레이아웃이 필요한 경우에는 주의가 필요합니다.
@GetMapping("/export/customers-auto")
public void exportCustomersWithAutoWidth(HttpServletResponse httpResponse) throws IOException {
httpResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
httpResponse.setCharacterEncoding("UTF-8");
String encodedFileName = URLEncoder.encode("고객_목록_자동너비", "UTF-8").replaceAll("\\+", "%20");
httpResponse.setHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".xlsx");
List<CustomerProfile> customers = customerService.fetchAllCustomers();
EasyExcel.write(httpResponse.getOutputStream(), CustomerProfile.class)
.sheet("고객 데이터")
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.doWrite(customers);
}
커스텀 동적 열 너비 전략 구현
기존 LongestMatchColumnWidthStyleStrategy의 한계를 보완하기 위해, 헤더 길이 또는 셀 데이터 길이를 기준으로 너비를 유동적으로 계산하는 커스텀 핸들러를 직접 구현할 수 있습니다. 생성자 파라미터를 통해 기준 모드를 선택하도록 설계합니다.
/**
* 헤더 또는 셀 콘텐츠 길이에 기반한 동적 열 너비 조절 전략
*/
public class AdaptiveColumnWidthStrategy extends AbstractColumnWidthStyleStrategy {
private static final int MAX_WIDTH_LIMIT = 255;
private static final int MIN_WIDTH_LIMIT = 15;
// 1: 헤더 기준, 2: 셀 데이터 기준
private final int widthCalculationMode;
private final Map> sheetWidthCache = new HashMap<>();
public AdaptiveColumnWidthStrategy(int mode) {
this.widthCalculationMode = mode;
}
@Override
protected void setColumnWidth(WriteSheetHolder sheetHolder, List> cellDataList,
Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
if (widthCalculationMode == 1 && isHead) {
int headerLength = cell.getStringCellValue().length();
int calculatedWidth = Math.max(headerLength * 2, MIN_WIDTH_LIMIT);
calculatedWidth = Math.min(calculatedWidth, MAX_WIDTH_LIMIT);
sheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), calculatedWidth * 256);
return;
}
boolean shouldProcess = isHead || (cellDataList != null && !cellDataList.isEmpty());
if (!shouldProcess) {
return;
}
Map columnWidthMap = sheetWidthCache.computeIfAbsent(
sheetHolder.getSheetNo(), k -> new HashMap<>()
);
int currentDataWidth = calculateContentWidth(cellDataList, cell, isHead);
if (currentDataWidth < 0) return;
currentDataWidth = Math.min(currentDataWidth, MAX_WIDTH_LIMIT);
Integer existingMaxWidth = columnWidthMap.get(cell.getColumnIndex());
if (existingMaxWidth == null || currentDataWidth > existingMaxWidth) {
columnWidthMap.put(cell.getColumnIndex(), currentDataWidth);
sheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), currentDataWidth * 256);
}
}
private int calculateContentWidth(List> cellDataList, Cell cell, Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes(StandardCharsets.UTF_8).length;
}
WriteCellData> cellData = cellDataList.get(0);
CellDataTypeEnum dataType = cellData.getType();
if (dataType == null) return -1;
switch (dataType) {
case STRING:
return cellData.getStringValue().getBytes(StandardCharsets.UTF_8).length + 2;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes(StandardCharsets.UTF_8).length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes(StandardCharsets.UTF_8).length * 2;
case DATE:
return cellData.getDateValue().toString().length() + 2;
default:
return -1;
}
}
}
커스텀 전략 적용 예시
아래는 구현한 AdaptiveColumnWidthStrategy를 모드에 따라 적용하여 엑셀 파일을 다운로드하는 컨트롤러 로직입니다.
@GetMapping("/export/customers/header-adaptive")
public void exportByHeaderWidth(HttpServletResponse httpResponse) throws IOException {
setupExcelResponse(httpResponse, "고객_목록_헤더적응형");
List<CustomerProfile> customers = customerService.fetchAllCustomers();
EasyExcel.write(httpResponse.getOutputStream(), CustomerProfile.class)
.sheet("데이터")
.registerWriteHandler(new AdaptiveColumnWidthStrategy(1)) // 모드 1: 헤더 기준
.doWrite(customers);
}
@GetMapping("/export/customers/content-adaptive")
public void exportByContentWidth(HttpServletResponse httpResponse) throws IOException {
setupExcelResponse(httpResponse, "고객_목록_콘텐츠적응형");
List<CustomerProfile> customers = customerService.fetchAllCustomers();
EasyExcel.write(httpResponse.getOutputStream(), CustomerProfile.class)
.sheet("데이터")
.registerWriteHandler(new AdaptiveColumnWidthStrategy(2)) // 모드 2: 셀 데이터 기준
.doWrite(customers);
}
private void setupExcelResponse(HttpServletResponse response, String fileName) throws UnsupportedEncodingException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
String encodedName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment;filename=" + encodedName + ".xlsx");
}