티스토리 뷰

Server

[Java] 엑셀 변환

니용 2019. 12. 3. 11:17
반응형

회사에서 개발하는 앱의 크기가 커지면서 관리자 화면에서 엑셀변환을 추출하는 것에 종종 OOM(Out Of Memory)이 일어났다. 그래서 자바 엑셀변환을 튜닝을 하게 되었는데, 튜닝 시 방법을 간단히 적어보려고 한다. 여기서 DI는 Apache Poi를 주로 사용했고, build.gradle 에 이와 같이 추가하였다.

 

compile('org.apache.poi:poi-ooxml:4.0.0')

 

기존의 로직은 아래와 같았다.

더보기

public static ResponseEntity<bytep[> exportExcel(String fileName, List<?> list
, LinkedHashMap format) {
    try {
        // 확장명 붙여주기
        if(!fileName.contains(".xlsx"))
            fileName += ".xlsx";
            
        // 파일명을 인코딩하지 않으면 한글의 경우 꺠져서 나온다
        String encordedFilename = URLEncoder.encode(fileName,"UTF-8").replace("+", "%20");
        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .header(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename="
                + encordedFilename + ";filename*= UTF-8''" + encordedFilename)
                .body(new ByteArrayOutputStream(){{
                
                    // 아파치 포이에서 제공하는 워크북이라는 클래스를 생성
                    XSSFWorkbook workbook = new XSSFWorkbook();
                    
                    // 시트를 하나 생성
                    XSSFSheet sheet = workbook.createSheet("sheet1");

                    AtomicInteger rowIndex = new AtomicInteger(0);
                    AtomicInteger cellIndex = new AtomicInteger(0);
                        
                    // 헤더row 생성
                    XSSFRow header = sheet.createRow(rowIndex.get());
                    
                    // 포맷에 들어간 Map에서 for loop를 돌면서 각 항목을 set
                    format.forEach((key, value) -> {
                        header.createCell(cellIndex.getAndIncrement(), CellType.STRING)
                                .setCellValue(value.toString());
                    });
                    
                    // 워크북에 입력
                    try {
                         workbook.write(this);
                         this.flush();
                    } catch (Exception e) {
                         log.error(ExceptionUtils.getStackTrace(e));
                    }
                 }}.toByteArray());
        
    }
    catch(Exception e) {}
    return null;
}

 

 

건수가 많지 않으면 위의 로직을 사용해도 전혀 무방하다. 5만 row까지는 괜찮은데, 이게 하다보면 다음과 같이 에러가 떨어진다.

 

OOM 에러 증상

 

이를 해결하기 위해서 엑셀변환이 아닌 csv 포맷으로 변환하는 것을 대책으로 결정이 났다. csv 파일은 excel로의 변환 과정이 없는 plain text를 그대로 출력하기 때문에 속도면에서나 쓰기면에서 빠른 것으로 알고 있다. 접목시켜본 결과 로직은 훨씬 심플하고 별다른 주입이 필요없이 개발이 진행되었다.

 

더보기

public static ResponseEntity exportCsv(String fileName, List list){
    HttpHeaders header = new HttpHeaders();
    try {
    
       // 위와 같이 파일명을 인코딩
       String encodedFileName = URlEncoder.encode(fileName, "UTF-8").replace("+", "%20");
       
       // 한글이 들어있다면 타입에 UTF-8 캐릭터셋을 입힘
       header.add("Content-Type", "text/csv; charset=UTF-8");
       
       // .csv로 확장자를 변환
       header.add("Content-Disposition", "attachment; filename=" + encodedFileName + ".csv");
    }
    catch(Exception e) {}
    
    // csv에 찍히는 헤더 
    StringBuilder sb = new StringBuilder("#,이름,전화번호,주소\n");
    for(User u : list)
        sb.append(u.toString());
    
    return new ReponseEntity(sb.toString(), header, HttpStatus.CREATED);
}


public class User {
    int userId;
    String name;
    String phone;
    String address;
    
    // comma(,)로 셀간 구분자를 넣어줌
    @Override
    public String toString(){
        return new StringBuilder("\n")
            .append(userId).append(",")
            .append(name).append(",")
            .append(phone).append(",")
            .append(address).toString();
    }
}

위의 소스의 원리는 간단하게도 하나의 문자열을 ResponseEntity에 파라미터로 넣어주면 된다. 근데 여기서 한가지 문제가 되었던 것은 엑셀 파일 안의 한글 깨짐 현상이었다. 분명 charset을 UTF-8로 설정을 하였음에도 불구하고 내부에서는 변환이 되지 않은 듯 하였다.

 

근데 여기서 헤더에 특수기호를 넣어주면 자연스럽게 해결이 되었다.

 

UTF-8 인코딩 적용

더보기

    // csv에 찍히는 헤더 
    StringBuilder sb = new StringBuilder("\ufeff#,이름,전화번호,주소\n");

 

왜 그런 것인지 명확하지는 않지만, 이렇게 해결하는 방법이 있었다. 어쨋든 이렇게 하여 5만 row 이상의 결과를 문제없이 출력할 수 있었다!

반응형

'Server' 카테고리의 다른 글

[Java] Cron 표현식  (0) 2019.12.19
SQL에 대해 알아보자  (0) 2019.12.15
[Java] 날짜 생성/변환  (0) 2019.12.06
[Java] ORM과 JPA, 그리고 Hibernate  (0) 2019.11.28
[Java] NPE와 Optional Class  (1) 2019.10.26
[Java] Lambda와 Stream(2)  (0) 2019.10.15
댓글
공지사항