[트러블 슈팅] 스프링 배치를 통해 엑셀데이터를 주입하기
해당글은 수강후기 공유 시스템의 스프링 배치 프로젝트를 진행한 과정을 설명하는 글이다.
배치 프로젝트의 목표는 엑셀 파일을 전달받았을때, 배치를 실행하여 데이터베이스와 데이터 일관성을 맞추는 것이다. 기존에 존재하는 데이터를 수정해주거나, 존재하지 않는 데이터는 추가해주는것이 목표라고 할 수 있다.
현재 스프링 배치 프로젝트는 각 프로젝트별로 생성되어있고 배치가 따로 돌아가는 구조를 가지고 있다.
├── batch-dummy
├── batch-enrollment
├── batch-lecture
├── batch-student
학과에 요청한 문서 양식 에 맞춰 데이터베이스에 주입하기위해서, 배치가 분리되었고 이런 과정에서 도메인 클래스 중복 사용 문제에 맞게 프로젝트별로 분리된 멀티모듈 구조를 사용하게 되었다. 또한, 배치를 분리한 이유중 하나는 데이터를 갱신하는 사이클을 생각해보았을때, 따로따로 하나씩만 돌아갈 수 있도록 하고싶었다. 하지만 Step 기준으로 한번에 묶어두면 굳이 DB I/O 를 추가로 발생시키면서 기존의 데이터를 확인하는 과정을 일으키고 싶지 않았다.
전체 배치가 실행되었을때, 데이터 갱신 사이클은 아래와 같다.
이 과정을 원활히 진행하기 위해서 전체 배치를 돌려야 할때만 사용할 수 있도록 간단한 쉘 스크립트를 작성해두었다. 분리된 배치의 모습을 조금더 자세히 설명하면, Step 성공시에 다음 Step으로 넘어가는 것을 고려했을떄, 데이터들간에 주입이 서로 Job간에 종속적이지 않으면 좋겠어서 배치를 여러개로 둔것이다.
교내 데이터는 엑셀에서 csv 파일로 추출하였고, 파일 양식에 맞춰서 클래스를 작성하였다.
@Getter @Setter
@ToString
public class ClassCsv {
private Long year;
private String term; // 학기
private Long classNumber;
private String course; // 이수 구분
private String name;
private String professor;
}
기본적으로 flat한 데이터이기때문에, 스프링 배치에 특별한 클래스들을 사용하지는 않았고, FlatFileItemReader와 ItemWriter를 커스텀하여 구현했다.
@Override
public void write(Chunk<? extends ClassCsv> chunk) throws Exception {
log.info("==== writer2 작동 (건수: {}) ====", chunk.size());
chunk.getItems().forEach(item -> {
try {
Lecture lecture = lectureDataRepository.findByLectureId(item.getClassNumber()).orElse(null);
if (lecture != null) {
log.info("교수명 업데이트: {} -> {}", lecture.getProfessor(), item.getProfessor());
lecture.updateProfessorName(item.getProfessor());
lectureDataRepository.save(lecture); // 저장 추가
i++;
} else {
log.warn("강의 ID {}를 찾을 수 없습니다.", item.getClassNumber());
}
} catch (Exception e) {
log.error("교수명 업데이트 실패: {}", item, e);
}
});
log.info("총 {}건 처리 완료", i);
}
각 스프링 배치 프로젝트가 복잡한 로직의 구현은 없고 기존의 값이 존재하면, 저장처리하는 식으로 진행이 되었다. 특히, Lecture
엔티티 클래스를 제어할때는, 연관관계가 2개이상 존재해 로직을 구현할때 분기점들이 구성되었다. 최초의 데이터 주입과 연관관계가 존재하는 데이터 주입은 복잡도가 달라진다는 것을 느끼게 되는 프로젝트였다.