개인 프로젝트를 하던중 많은 데이터를 db 에 insert 해야 하는 상황이 발생했다. 이전 협업 프로젝트때에 사용했던 엑셀 업로드 로직을 내 필요에 맞게 커스터마이징하여 편리하게 사용하게 됐다 .
그래서 Spring Boot에서 Apache POI 라이브러리를 사용하여 엑셀 데이터를 Java 객체로 변환하는 과정을 정리해보려 한다 .
Controller → Extractor → Service → Creator → 도메인 서비스
Controller: 클라이언트로부터 엑셀 파일을 받아 처리를 시작
Extractor: 엑셀 파일 데이터를 읽어 DTO 객체로 변환
Service: 추출된 DTO 목록을 순회하며 등록 처리
Creator: 실제 도메인 서비스를 호출하여 데이터 생성
도메인 서비스: 비즈니스 로직 수행 및 데이터베이스 저장
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;
}
};
}
@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);
}
// 다른 엑셀 처리 메소드들...
}
파일 업로드: 클라이언트가 엑셀 파일을 업로드합니다.
워크북 생성: Apache POI를 사용하여 엑셀 파일로부터 워크북객체 를 생성합니다.
데이터 추출: 워크북에서 첫 번째 시트를 읽어 행 단위로 처리합니다.
객체 매핑: 각 행의 데이터를 DTO 객체로 변환합니다.
유효성 검사: 데이터의 유효성을 검사하고 문제가 있으면 예외를 발생시킵니다.
등록 처리: 변환된 DTO 객체를 사용하여 시스템에 데이터를 등록합니다.
결과 반환: 처리 결과를 클라이언트에 반환합니다.
타입 불일치: 엑셀 셀의 데이터 타입이 예상과 다른 경우
빈 셀: 필수 값이 비어있는 경우
유효성 검사 실패: 데이터가 비즈니스 규칙에 맞지 않는 경우
IO 예외: 파일 읽기/쓰기 중 발생하는 예외