Spring Apache-poi Excel(엑셀) Export(출력)(1/2) - 어노테이션 정의

YouMakeMeSmile·2021년 6월 9일
3

현재 서비스중인 레거시의 화면은 miplatform으로 개발되어 있으며 Grid Excel Export의 경우 화면에 출력된 Grid의 데이터와 스타일 그대로 Excel로 출력해준다.

우선 화면에서의 Grid 출력은 현재 사용 예정인 ag-grid에서 제공하는 export를 이용하기로 하였으며 나는 화면 Grid 출력이외의 서버에서의 export를 구현하기로 하였다. 반드시 ag-grid를 사용하지 않는 화면도 존재 할것이므로 최대한 많은 기능을 포함시키는것을 목표로 했다.


이번에도 역시 다른 멋진 개발자분들의 경험과 시행착오들을 빌리기 위해 어김없이 구글을 통해 여러 적용기를 확인하였고 우아한블로그에 최태현 개발자님의 작성하신 글(아 엑셀다운로드 개발,,, 쉽고 빠르게 하고 싶다 (feat. 엑셀 다운로드 모듈 개발기))이 많은 도움이 되었다.

먼저 생각한 것은 내가 작성한 코드이기 때문에 나는 해당 사용방법에 납득이 되지만 다른 개발자분들의 입장에서 최대한 납득이 될 수 있는 구조의 Excel Export를 제공하고 싶었다. 하지만 내가 다른 개발자의 입징이라면 납득하지 못할 것이다.


레거시의 Excel Export는 위에서 이야기한것 처럼 Grid를 그대로 Excel Export 하기 때문에 정말 형태가 어마무시한 경우가 존재하였다.

  1. 헤더가 한줄이 아닌 경우가 존재한다.
  2. 위의 경우에 더불어 데이터도 한줄이 아닌 경우가 존재한다.
  3. 헤더는 셀 두개가 병합되어있지만 데이터부에는 각각의 셀에 데이터가 존재한다.해당 ui가 정말 이해되지 않는다.
  4. 해당 셀의 이전행의 데이터와 현재행의 데이터가 같으면 병합된다.
  5. 그리고 기본적인 배경색, 글자색, 포멧등 기본적인 스타일이 출력된다.


Excel 라이브러리는 고민없이 poi를 선택하였으며 위에 소개한 최태현 개발자님의 글(아 엑셀다운로드 개발,,, 쉽고 빠르게 하고 싶다 (feat. 엑셀 다운로드 모듈 개발기))에서 처럼 클래스의 필드에 어노테이션을 적용하여 Excel구성을 하기로 결정하였다.

레거시의 Excel 형태는 어마무시하여 최태현 개발자님이 작성하신 글의 내용보다 어노테이션에 포함시켜야 할 정보들이 더욱 많았다.

우선 헤더의 행과 데이터의 행이 반드시 일치 하지 않은 경우가 존재했다. 이러한 이유로 @ExcelHeader@ExcelBody를 구분하였다. 덕분에 한 필드에 두개의 어노테이션을 추가하는 선택을 하였다.


@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelHeader {
    String headerName();
    int rowIndex() default 0;
    int colIndex();
    int colSpan() default 0;
    int rowSpan() default 0;
    HeaderStyle headerStyle() default @HeaderStyle;
}

@ExcelHeader의 경우에는 칼럼명, 인덱스를 필수값으로 입력을 받으며 필요에 따라 행병합[rowSpan], 열병합[colSpan], 스타일[headerStyle]이 존재하는 경우 정의하도록 하였다.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelBody {
    int rowIndex() default 0;
    int colIndex();
    int colSpan() default 0;
    int rowSpan() default 0;
    int width() default 8;
    boolean rowGroup() default false;
    BodyStyle bodyStyle() default @BodyStyle;
}

@ExcelBody는 칼럼의 인덱스만 필수값이며 필요에 따라 행병합[rowspan], 열병합[colSpan], 넓이[width], 그룹여부[rowGroup], 스타일[bodyStyle]이 존재하는 경우 정의하도록 하였다.

헤더 행과 데이터 행의 기본 적용되는 스타일의 다른부분이 존재하며 설정되는 속성도 다른 부분이 존재하여 스타일 또한 @HeaderStyle@BodyStyle로 구분하여 정의하였다.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HeaderStyle {
    Background background() default @Background;
    int fontSize() default 11;
    HorizontalAlignment horizontalAlignment() default HorizontalAlignment.CENTER;
    VerticalAlignment  verticalAlignment() default VerticalAlignment.CENTER;
}

@HeaderStyle 속성 자체가 Optional한 속성이기 때문에 필수 값은 존재하지 않으며 배경[background], 글씨크기[fontSize], 수평정렬[horizontalAlignment], 수직정렬[verticalAlignment] 를 지정할 수 있도록 하였다.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BodyStyle {
    Background background() default @Background;
    int fontSize() default 11;
    HorizontalAlignment horizontalAlignment() default HorizontalAlignment.GENERAL;
    VerticalAlignment verticalAlignment()default VerticalAlignment.CENTER;
    String numberFormat() default "";
    String dateFormat() default "YYYY-MM-DD HH:mm:ss";
}

@BodyStyle도 마찬가지로 Optional한 속성이기 때문에 필수 값은 존재하지 않으며 배경[background], 글씨크기[fontSize], 수평정렬[horizontalAlignment], 수직정렬[verticalAlignment], 숫자표현형태[numberFormat], 날짜표현형태[dateFormat] 를 지정할 수 있도록 하였다.


이러한 annotation을 통해 ExcelExport하기 위한 Class 파일을 다음과 같은 예시처럼 정의하면 된다.

@Setter
@Getter
@NoArgsConstructor
public class Sample1 {
    @ExcelHeader(headerName = "반", colIndex = 0, rowIndex = 0, rowSpan = 1, headerStyle = @HeaderStyle(background = @Background("#ECEFF3")))
    @ExcelBody(rowIndex = 0,colIndex = 0,rowGroup = true, bodyStyle = @BodyStyle(horizontalAlignment = HorizontalAlignment.CENTER))
    private String seq;

    @ExcelHeader(headerName = "이름", colIndex = 1, rowIndex = 0, rowSpan = 1, headerStyle = @HeaderStyle(background = @Background("#ECEFF3")))
    @ExcelBody(rowIndex = 0, colIndex = 1, rowSpan = 1)
    private String name;

    @ExcelHeader(headerName = "국어/수학", colIndex = 2, colSpan = 1, rowIndex = 0, headerStyle = @HeaderStyle(background = @Background("#ECEFF3")))
    private String koreanMathHeader;

    @ExcelBody(rowIndex = 0, colIndex = 2)
    private String korean;

    @ExcelBody(rowIndex = 0, colIndex = 3)
    private String math;

    @ExcelHeader(headerName = "영어", colIndex = 2, rowIndex = 1, headerStyle = @HeaderStyle(background = @Background("#ECEFF3")))
    @ExcelBody(rowIndex = 1, colIndex = 2)
    private String english;

    @ExcelHeader(headerName = "역사", colIndex = 3, rowIndex = 1, headerStyle = @HeaderStyle(background = @Background("#ECEFF3")))
    @ExcelBody(rowIndex = 1, colIndex = 3)
    private String history;

    @ExcelHeader(headerName = "생일", colIndex = 4, rowIndex = 0, headerStyle = @HeaderStyle(background = @Background("#ECEFF3")))
    @ExcelBody(rowIndex = 0, colIndex = 4, bodyStyle = @BodyStyle(dateFormat = "YYYY-MM-DD"), width = 12)
    private LocalDateTime birthDay;

    @ExcelHeader(headerName = "금액", colIndex = 4, rowIndex = 1, headerStyle = @HeaderStyle(background = @Background("#ECEFF3")))
    @ExcelBody(rowIndex = 1, colIndex = 4, bodyStyle = @BodyStyle(numberFormat = "#,000원"))
    private BigDecimal money;

    public Sample1(String seq, String name, String korean, String math, String english, String history, LocalDateTime birthDay, BigDecimal money) {
        this.seq = seq;
        this.name = name;
        this.korean = korean;
        this.math = math;
        this.english = english;
        this.history = history;
        this.birthDay = birthDay;
        this.money = money;
    }
}

주의 해야할 내용으로는 행병합 또는 열병합이 있을경우 index를 계산하여 다음 행, 열의 index를 지정해야 한다.

위와 같이 정의한 후 다음 글에서 다룰 해당 클래스에 명시한 @ExcelHeader, @ExcelBody의 정보를 통해 Apache-poi를 이용하여 Excel을 출력한 결과는 다음과 같다.


생각보다 글의 내용이 많아져서 내용을 나누어 작성하려고 한다. 현재 첫 번째 글에서는 Excel을 나태나기 위한 정보? 메타데이터? 구성하는 방식에 어노테이션을 사용한 내용을 주로 다루었으며 다음 글에서 해당 어노테이션들을 이용하여 전달받은 데이터와 어노테이션의 정보를 통해 실제 엑셀을 출력하는 로직을 다루도록 하겠다.

profile
어느새 7년차 중니어 백엔드 개발자 입니다.

0개의 댓글