EasyExcel 열 너비 자동 조정 및 커스터마이징 전략

어노테이션을 이용한 고정 열 너비 설정

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");
}

태그: EasyExcel java ApachePOI SpringBoot ExcelExport

7월 12일 00:30에 게시됨