1. 개요
CDSS는 임상 의사 결정 지원 시스템의 줄임말로서 환자로 부터 얻어진 임상 정보를 바탕으로 의료인들이 질병을 진단하고 치료할 때 의사 결정을 도와주는 시스템입니다.
저는 FHIR 의료 데이터를 받아서 가공한 후 각각에 맞는 알고리즘에 전송하는 시스템을 개발 중에 있습니다. 중요한건 환자 데이터가 몇 십만 건이 됐을 때 이를 빠르게 처리해야 된다는 점입니다.
FHIR 데이터의 예시
회의록
2. 설계하기
우선 시스템의 대략적인 설계는 다음과 같습니다.
•
스케쥴링 (Spring Scheduler, Quartz) 을 사용해 주기적으로 의료 데이터 API를 호출합니다.
•
의료 데이터 API에서는 JSON 형식으로 된 압축 파일을 반환합니다.
•
압축 데이터를 풉니다.
•
복구된 JSON 데이터를 자바 객체로 직렬화 하고, 세분화된 객체로 다시 한번 정제합니다.
•
각각의 객체를 데이터베이스 테이블에 저장합니다.
해당 작업은 일련의 메소드로 구성해도 무방해 보이지만, 고려할 것은 대용량 트래픽입니다.
많은 수의 FHIR 데이터가 서버로 전송될 때 이를 분류하고 각각의 테이블에 집어 넣는 과정은 효율적으로 이루어져야 합니다.
따라서 스프링 배치를 도입하고자 하였습니다.
3. 스프링 배치
스프링 배치를 선택한 이유는 다음과 같습니다.
•
많은 수의 데이터를 효율적으로 처리하는데 있어서 프레임워크 단에서 설정을 쉽게 건들 수 있는 것은 스프링 배치이다.
•
일련의 작업들을 Step 단위로 쪼개서 구현 가능하다.
3-1. unCompressZipStep
처음에 생각했던 배치의 흐름입니다.
먼저 압축된 파일 (zip) 을 어떻게 읽어야 할까요?
우선 자바에서 zip 파일을 다루기 위해서는 java.util.zip 라이브러리를 살펴봐야 합니다.
zipFileItemReaderV1
@Bean
@StepScope
public ItemReader<ZipEntry> zipFileItemReader(
@Value("#{jobParameters[filePath]}") String zipFilePath) throws IOException {
// zip 파일 압축 풀기
ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(zipFilePath));
ZipEntry zipEntry = zipInputStream.getNextEntry();
List<ZipEntry> zipEntries = new ArrayList<>();
while(zipEntry != null) {
String filePath = zipFilePath + File.separator + zipEntry.getName();
if(!zipEntry.isDirectory() && zipEntry.getName().endsWith(".json")){
// Json 파일을 처리
zipEntries.add(new ZipEntry(filePath));
}
zipInputStream.closeEntry();
zipEntry = zipInputStream.getNextEntry();
}
zipInputStream.close();
return new ListItemReader<>(zipEntries);
Java
복사
처음에는 단순히 zip 안에 있는 json 파일들을 하나씩 ZipEntry 객체로 꺼내 Processor로 넘겨 처리하려고 했습니다.
근데 ZipEntry는 파일의 메타 데이터를 담는 객체일 뿐이지 결국 파일의 내용을 얻기 위해서는 ZipInputStream이 필요하다는 사실을 깨달았습니다. 결국 우리가 원하는건 압축된 zip 파일 안에 있는 JSON 파일의 데이터이기 때문입니다.
@Bean
@StepScope
public ItemReader<InputStream> zipFileItemReader(
@Value("#{jobParameters[filePath]}") String zipFilePath) throws IOException {
// zip 파일 압축 풀기
ZipFile zipFile = new ZipFile(zipFilePath);
Enumeration<? extends ZipEntry> entries = zipFile.entries();
List<InputStream> inputStreams = new ArrayList<>();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (!entry.isDirectory() && entry.getName().endsWith(".json")) {
inputStreams.add(zipFile.getInputStream(entry));
}
}
return new ListItemReader<>(inputStreams);
}
@Bean
public ItemProcessor<InputStream, String> zipEntryToStringProcessor() {
return inputStream -> {
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
}
return stringBuilder.toString();
};
}
Java
복사
그래서 파일의 InputStream 자체를 Processor로 넘겨주고 Processor에서 해당 Stream을 처리하면서 JSON 데이터를 추출하는 것으로 구현하였습니다.
다만, JSON 데이터가 배열로 이루어져 길어질 경우 StringBuilder는 메모리에 큰 부담이 갈 수 있습니다. 이 때, Jackson 라이브러리를 활용하여 SaxParser 와 같이 이벤트 단위로 처리할 수 있다는 사실을 알았습니다.