프로젝트 내에서 엑셀 파일 업로드를 어떻게 처리하고 DB에 저장하는지를 정리해봤다.
엑셀 파일 업로드 → 데이터 파싱 → 중간 테이블 저장 → 배치 처리 → 최종 테이블 저장의 흐름으로 처리된다.
전체 구조는 다음과 같다.
엑셀 파일
↓
EasyExcel로 데이터 읽기
↓
헤더 검증
↓
index 기반 컬럼 매핑
↓
중간 테이블 저장 (ifu_*)
↓
Spring Batch 처리
↓
최종 테이블 저장 (hm_member_*)
1. 엑셀 파일 업로드 및 읽기
엑셀 파일은 이미 Object Storage에 업로드되어 있으며, 업로드 정보 테이블에서 파일 경로를 가져온다.
CheckupUploadFilecheckupUploadFile=
checkupUploadFileService.selectCheckupUploadFileInfo(checkupUploadFile);
Stringkey=checkupUploadFile.getFilePath();
예시 경로
admin/wkuh/checkup/resultSpc/파일명.xlsx
이 경로를 이용해 EasyExcel 라이브러리로 엑셀 데이터를 읽는다.
List<List<String>>list=easyExcelComponent.readRows(key);
EasyExcel은 엑셀을 다음과 같은 구조로 변환한다.
List<List<String>>
즉,
- List → 행(row)
- 내부 List → 컬럼(column)
예를 들어 다음과 같은 엑셀 데이터가 있다면
성명 검사일 부서명
| 홍길동 | 20250101 | 개발팀 |
| 김철수 | 20250102 | 기획팀 |
EasyExcel은 이를 다음 구조로 변환한다.
[
["성명", "검사일", "부서명"],
["홍길동", "20250101", "개발팀"],
["김철수", "20250102", "기획팀"]
]
내부적으로는 Listener가 먼저 Map<Integer, String> 형태로 데이터를 받은 뒤
최종적으로 List<List<String>> 구조로 변환한다.
2. 헤더 검증
엑셀 데이터의 첫 번째 행은 헤더이기 때문에, 시스템에서 기대하는 컬럼 구조와 일치하는지 검증한다.
StringresultHeader=
checkupUploadFileService.validateTitle(
EXPECT_HEADER,
list.getFirst(),
checkupUploadFile
);
if (resultHeader!=null) {
thrownewServiceException(resultHeader);
}
검증 기준이 되는 헤더는 미리 정의되어 있다.
privatestaticfinalList<String>EXPECT_HEADER=List.of(
"성명",
"검사일",
"부서명",
"생년월일",
"사원번호"
);
검증이 완료되면 헤더 행은 제거한다.
list.removeFirst();
3. index 기반 컬럼 순차 읽기
헤더를 제거한 뒤 각 행을 순회하면서 index를 증가시키며 컬럼을 읽는다.
for (List<String>row :list) {
intindex=0;
${hospital}CheckupResultSpcresultFile=new${hospital}CheckupResultSpc();
resultFile.setEmployeeName(
EasyExcelUtil.getString(row,index++)
);
resultFile.setCheckupDt(
EasyExcelUtil.getString(row,index++)
);
resultFile.setDeptName(
EasyExcelUtil.getString(row,index++)
);
}
여기서 index++는 후위 증가 연산자이기 때문에 다음과 같은 순서로 동작한다.
현재 index로 값 읽기
→ index + 1 증가
예를 들어 다음과 같은 데이터가 있다면
["홍길동", "20250101", "개발팀", "19900101", "E001"]
읽히는 순서는 다음과 같다.
index = 0 → 홍길동
index = 1 → 20250101
index = 2 → 개발팀
index = 3 → 19900101
index = 4 → E001
EasyExcelUtil.getString()은 다음 역할을 한다.
- row.get(index)로 값 조회
- 인덱스 범위를 벗어나면 null 반환
- 앞뒤 공백 제거
- 엑셀 문자열 앞의 ' 제거
4. 중간 테이블 저장
엑셀 데이터를 바로 최종 테이블에 저장하지 않고
먼저 중간 테이블에 적재한다.
테이블
resultFile.setDealYn("N");
resultFile.setUseYn("Y");
CheckupResultSpcMapper.insertHospitalCheckupResultSpc(resultFile);
이 단계는 Spring Batch Job의 Step1에서 수행된다.
중간 테이블을 사용하는 이유는 다음과 같다.
- 원본 데이터 보존
- 배치 처리 안정성 확보
- 데이터 검증 및 재처리 가능
5. 배치 처리로 최종 테이블 저장
전체 배치는 3개의 Step으로 구성되어 있다.
Step 1
엑셀 → 중간 테이블 (병원별 결과를 다룬다)
${hospital}CheckupResultSpcTasklet
엑셀 데이터를 읽어 ${hospital}_checkup_result_spc 테이블에 저장한다.
Step 2
중간 테이블 → 최종 테이블
${hospital}MemberCheckupResultSpcTasklet
중간 테이블 데이터를 읽어 최종 테이블 구조로 변환한다.
List<${hospital}CheckupResultSpc>list=
${hospital}CheckupResultSpcMapper.select${hospital}CheckupResultSpcList(request);
for (${hospital}CheckupResultSpcitem :list) {
MemberCheckupResultSpcmcrs=newMemberCheckupResultSpc();
mcrs.setHeight(item.getHeight());
mcrs.setWeight(item.getWeight());
mcrs.setBmi(item.getBmi());
memberCheckupResultSpcMapper.upsertMemberCheckupResultSpc(mcrs);
}
최종 저장 테이블
hm_member_checkup_result_spc
여기서는 UPSERT 방식을 사용한다.
즉,
데이터가 없으면 INSERT
이미 존재하면 UPDATE
Step 3
추가 검진 결과 처리
${hospital}MemberCheckupResultTasklet
검진 결과와 관련된 추가 후처리를 수행한다.
정리
이 구조의 핵심 특징은 다음과 같다.
1. Index 기반 컬럼 매핑
헤더 순서가 고정되어 있기 때문에
index++ 방식으로 빠르게 컬럼을 매핑한다.
2. 2단계 저장 구조
엑셀
→ 중간 테이블 (ifu_*)
→ 최종 테이블 (hm_member_*)
중간 테이블을 통해 데이터 안정성을 확보한다.
3. Spring Batch 기반 대용량 처리
배치에서 데이터를 limit 단위로 나누어 처리하여
대량 검진 데이터를 안정적으로 처리할 수 있다.
4. UPSERT 저장 방식
최종 테이블에서는 ON CONFLICT 기반 UPSERT를 사용해
중복 데이터를 자동으로 업데이트한다.
이 과정을 통해 엑셀로 업로드된 검진 데이터는
최종적으로 hm_member_checkup_result_spc 테이블에 저장된다.
'Dev Log' 카테고리의 다른 글
| 개발자가 알아야 하는 A/B 테스트와 관련 지표 (0) | 2026.03.22 |
|---|---|
| 날씨 기능 간단한 거 아냐? 공공 API 연동하기 삽질 과정 (0) | 2026.03.15 |
| 무한스크롤을 적용하며 다시 보게 된 페이징 구조 (0) | 2026.03.01 |
| 파일 업로드: “썸네일/메인 이미지” 업로드에서 두 필드가 동기화되는 버그 트러블슈팅 (1) | 2026.02.22 |
| Java 컬렉션 프레임워크 정리: List, Set, Map, HashMap (0) | 2026.02.15 |