본문 바로가기

Dev Log

엑셀 업로드부터 DB 저장까지의 전체 프로세스

728x90

프로젝트 내에서 엑셀 파일 업로드를 어떻게 처리하고 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 테이블에 저장된다.

728x90