엑셀 데이터 -> Java 객체(Apache POI 활용)

SUUUI·2025년 3월 21일
0

개인 프로젝트를 하던중 많은 데이터를 db 에 insert 해야 하는 상황이 발생했다. 이전 협업 프로젝트때에 사용했던 엑셀 업로드 로직을 내 필요에 맞게 커스터마이징하여 편리하게 사용하게 됐다 .

그래서 Spring Boot에서 Apache POI 라이브러리를 사용하여 엑셀 데이터를 Java 객체로 변환하는 과정을 정리해보려 한다 .

  1. 전체 아키텍처 구조
Controller → Extractor → Service → Creator → 도메인 서비스

Controller: 클라이언트로부터 엑셀 파일을 받아 처리를 시작
Extractor: 엑셀 파일 데이터를 읽어 DTO 객체로 변환
Service: 추출된 DTO 목록을 순회하며 등록 처리
Creator: 실제 도메인 서비스를 호출하여 데이터 생성
도메인 서비스: 비즈니스 로직 수행 및 데이터베이스 저장

  1. 핵심 컴포넌트
  • ExcelDataExtractor (추상화된 기본 클래스)
    엑셀 데이터 추출을 위한 추상 클래스로, 공통 로직을 담고 있다
    java복사public class ExcelDataExtractor {
    public List extract(Sheet sheet) {
    Iterator rowIterator = sheet.rowIterator();

        // 제목줄인 첫 행은 건너 뛴다.
        if (rowIterator.hasNext()) {
            rowIterator.next();
        }
    
        StringBuilder exceptionMessage = new StringBuilder();
        int index = 1;
    
        List<T> list = new ArrayList<>();
        while (rowIterator.hasNext()) {
            try {
                index++;
                Row row = rowIterator.next();
                // 빈 행인 경우 건너뛰기
                if (isEmptyRow(row)) {
                    continue;
                }
                list.add(map(row));
            } catch (IllegalStateException e) {
                exceptionMessage.append(index).append("행: ").append("값의 타입이 맞지않습니다.\n");
            } catch (NullPointerException e) {
                exceptionMessage.append(index).append("행: ").append("셀에 값이 존재하지 않습니다.\n");
            } catch (IllegalArgumentException e) {
                exceptionMessage.append(index).append("행: ").append(e.getMessage()).append("\n");
            }
        }
    
        if (!exceptionMessage.isEmpty()) {
            throw new IllegalArgumentException(exceptionMessage.toString());
        }
    
        return list;
    }
    
    protected T map(Row row) {
        return null;
    }
    
    // 행이 비어있는지 확인하는 메소드
    private boolean isEmptyRow(Row row) {
        if (row == null) {
            return true;
        }
        for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) {
            if (row.getCell(i) != null && !row.getCell(i).toString().trim().isEmpty()) {
                return false;
            }
        }
        return true;
    }

    }

  • 구체적인 Extractor 구현 (GymDataExcelExtractor)
    특정 도메인(헬스장)에 대한 실제 추출 로직을 구현

@Slf4j
public class GymDataExcelExtractor {
    public static List<CreateGymDTO> extract(MultipartFile file) {
        try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(file.getBytes()))) {
            ExcelDataExtractor<CreateGymDTO> extractor = getExtractor();
            return extractor.extract(workbook.getSheetAt(0));
        } catch (IOException e) {
            throw new RuntimeException("엑셀 파일을 읽는 중 오류가 발생했습니다.", e);
        }
    }

    private static ExcelDataExtractor<CreateGymDTO> getExtractor() {
        return new ExcelDataExtractor<CreateGymDTO>() {
            private final DataFormatter dataFormatter = new DataFormatter();

            @Override
            protected CreateGymDTO map(Row row) {
                int rowNum = row.getRowNum() + 1;

                String imagePathInfo = row.getCell(6).getStringCellValue().trim();
                List<String> imageList = getExcelImageList(imagePathInfo);

                // 이미지 URL의 확장자 유효성 검사
                validateImageExtensions(imageList, rowNum);
                
                CreateGymDTO dto = CreateGymDTO.builder()
                        .gymName(row.getCell(0).getStringCellValue().trim())
                        .dailyPrice((long) row.getCell(1).getNumericCellValue())
                        .monthlyPrice((long) row.getCell(2).getNumericCellValue())
                        .address(row.getCell(3).getStringCellValue().trim())
                        .description(row.getCell(4).getStringCellValue().trim())
                        .localId((long) row.getCell(5).getNumericCellValue())
                        .uploadFileNames(imageList)
                        .info(row.getCell(7).getStringCellValue().trim())
                        .popular(row.getCell(8).getStringCellValue().trim())
                        .build();

                return dto;
            }
        };
    }
  • 엑셀 데이터 처리 서비스 (GymExcelService)
    추출된 DTO 목록을 처리하는 서비스 클래스
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class GymExcelService {

    private final GymRepository gymRepository;
    private final CustomFileUtil fileUtil;
    private final GymCreator gymCreator;

    public Long register(List<CreateGymDTO> dtoList) {
        log.info("register dtoList: {}", dtoList);
        for (int i = 0; i < dtoList.size(); i++) {
            try {
                gymCreator.create(dtoList.get(i));
            } catch (IllegalArgumentException e) {
                // 1행 부터 시작이기 때문에 2를 더한다.
                throw new IllegalArgumentException(i + 2 + "행에서 문제발생, " + e.getMessage());
            } catch (Exception e) {
                throw new IllegalArgumentException(i + 2 + "행에서 Fail문제 발생, " + e.getMessage());
            }
        }

        return (long) dtoList.size();
    }
}

2.4 데이터 생성기 (GymCreator)
실제 도메인 서비스를 호출하여 데이터를 생성

@RequiredArgsConstructor
@Service
public class GymCreator {
    private final AdminService adminService;

    public void create(CreateGymDTO CreateGymDTO) {
        adminService.createGym(CreateGymDTO);
    }
}

컨트롤러 (ExcelController)
클라이언트로부터 엑셀 파일을 받아 처리를 시작합니다.

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/excel")
@Tag(name = "Excel API", description = "엑셀을 통한 대량 데이터 등록 API")
public class ExcelController {

    private final TrainerExcelService trainerExcelService;
    private final GymExcelService excelService;
    private final MemberExcelService memberExcelService;

    @Operation(
            summary = "헬스장 데이터 엑셀 등록",
            description = "엑셀 파일을 업로드하여 헬스장 데이터를 대량 등록합니다."
    )
    @ApiResponse(responseCode = "201", description = "엑셀 데이터 등록 성공")
    @PostMapping("/gym")
    public ResponseEntity<?> registerByExcel(
            @RequestPart(value = "file") MultipartFile batchRegistrationFile
    ) {
        List<CreateGymDTO> registrationDtoList = GymDataExcelExtractor.extract(batchRegistrationFile);
        Long registeredSize = excelService.register(registrationDtoList);
        return new ResponseEntity<>(registeredSize + "개 row 데이터 excel 등록 완료!", HttpStatus.CREATED);
    }
    
    // 다른 엑셀 처리 메소드들...
}
  1. 데이터 변환 흐름

파일 업로드: 클라이언트가 엑셀 파일을 업로드합니다.
워크북 생성: Apache POI를 사용하여 엑셀 파일로부터 워크북객체 를 생성합니다.
데이터 추출: 워크북에서 첫 번째 시트를 읽어 행 단위로 처리합니다.
객체 매핑: 각 행의 데이터를 DTO 객체로 변환합니다.
유효성 검사: 데이터의 유효성을 검사하고 문제가 있으면 예외를 발생시킵니다.
등록 처리: 변환된 DTO 객체를 사용하여 시스템에 데이터를 등록합니다.
결과 반환: 처리 결과를 클라이언트에 반환합니다.

  1. 예외 처리
    엑셀 데이터 처리 중 발생할 수 있는 다양한 예외를 처리합니다:

타입 불일치: 엑셀 셀의 데이터 타입이 예상과 다른 경우
빈 셀: 필수 값이 비어있는 경우
유효성 검사 실패: 데이터가 비즈니스 규칙에 맞지 않는 경우
IO 예외: 파일 읽기/쓰기 중 발생하는 예외

profile
간단한 개발 기록

0개의 댓글