본문 바로가기
Spring/Batch

[Spring Batch] 스프링 배치 JSON 읽기/쓰기 전략

by coding_whale 2026. 5. 11.
반응형

현대적인 시스템 간 통신과 데이터 저장에서 JSON(JavaScript Object Notation)은 사실상의 표준이다. 중첩 구조를 표현하기 쉽고 유연하여 REST API 응답이나 NoSQL 저장소에서 핵심적으로 사용된다.

스프링 배치는 이 중괄호({}) 속에 담긴 데이터를 대량으로 처리하기 위해 전용 리더와 라이터를 제공한다. 이번 포스팅에서는 단순한 한 줄 JSON부터 복잡한 배열 구조까지, 실전에서 마주치는 모든 JSON 시나리오를 파악해본다.

 

1. JSON 처리의 두 갈래: JSONL vs JSON Array

실무에서 JSON 파일을 다룰 때는 데이터가 저장된 물리적 구조를 먼저 파악해야 한다. 이에 따라 사용하는 무기(Reader)가 달라지기 때문이다.

🔍 포맷 분석

  1. JSONL (JSON Lines): 한 줄에 하나의 JSON 객체가 기록된 형식이다. 파일 전체를 파싱할 필요 없이 한 줄씩 읽어 처리할 수 있어 대용량 로그나 스트리밍 데이터 처리에 유리하다.
  2. JSON Array: 파일 전체가 하나의 거대한 대괄호([])로 감싸진 배열 형식이다. 전통적인 설정 파일이나 데이터 내보내기 결과물에서 자주 발견된다.

 

 

2. JSONL(JSON Lines) 읽기 - 커스텀 LineMapper

JSONL은 구조적으로 '플랫 파일'과 유사하다(개행 문자로 레코드가 구분됨). 따라서 FlatFileItemReader를 활용하되, 문자열을 객체로 바꾸는 로직에 ObjectMapper를 주입하여 처리하는 것이 가장 효율적이다.

💻 실전 코드: 커스텀 LineMapper 구현

@Bean
@StepScope
public FlatFileItemReader<SystemFailure> systemFailureItemReader(
        @Value("#{jobParameters['inputFile']}") String inputFile) {
    return new FlatFileItemReaderBuilder<SystemFailure>()
            .name("jsonlReader")
            .resource(new FileSystemResource(inputFile))
            // [핵심] 람다를 사용하여 ObjectMapper로 각 라인을 즉시 객체화
            .lineMapper((line, lineNumber) -> objectMapper.readValue(line, SystemFailure.class))
            .build();
}
  • ObjectMapper: 스프링 부트가 자동으로 관리하는 빈을 주입받아 사용하면 spring-boot-starter-json에 정의된 각종 직렬화 설정(날짜 포맷 등)이 그대로 유지된다.

 

 

3. 여러 줄에 걸친 JSON 처리

데이터 가독성을 위해 JSON 객체가 여러 줄에 걸쳐 포맷팅(Pretty Print)된 경우, 단순한 라인 단위 읽기는 실패한다. 이때는 RecordSeparatorPolicy라는 특수 탐지기를 장착해야 한다.

🛠️ JsonRecordSeparatorPolicy의 내부 원리

이 정책은 단순히 줄바꿈을 찾는 것이 아니라 중괄호의 짝을 검사한다.

  1. 여는 중괄호({)와 닫는 중괄호(})의 개수가 일치하는가?
  2. 현재 라인이 닫는 중괄호(})로 끝나는가?

위 조건이 충족될 때까지 여러 줄을 계속 읽어 하나의 '레코드'로 결합한다.

.recordSeparatorPolicy(new JsonRecordSeparatorPolicy()) // 여러 줄 JSON 감지기 장착

 

 

4. JSON 배열 장악 - JsonItemReader

파일 전체가 하나의 배열로 되어 있다면 FlatFileItemReader로는 한계가 있다. 이때는 전용 무기인 JsonItemReader를 소환한다.

🛠️ 구성 요소

  • JsonObjectReader: 실제 파싱을 담당하는 심장부다. JacksonJsonObjectReader나 GsonJsonObjectReader 중 선택할 수 있다.
  • Streaming 방식: JsonItemReader는 파일 전체를 메모리에 올리지 않고, 스트리밍 방식으로 배열 요소를 하나씩 읽어 들인다. 따라서 기가바이트 단위의 JSON 배열도 안전하게 처리 가능하다.

 

💻 실전 코드: Jackson 기반 JSON 리더

@Bean
@StepScope
public JsonItemReader<SystemFailure> systemFailureJsonReader(
        @Value("#{jobParameters['inputFile']}") String inputFile) {
    return new JsonItemReaderBuilder<SystemFailure>()
            .name("jsonArrayReader")
            .resource(new FileSystemResource(inputFile))
            // [핵심] Jackson 라이브러리를 사용해 SystemFailure 타입으로 읽기 지정
            .jsonObjectReader(new JacksonJsonObjectReader<>(SystemFailure.class))
            .build();
}

 

 

5. 데이터를 JSON 배열로 변화 - JsonFileItemWriter

배치 처리 결과를 표준화된 JSON 형식으로 내보내야 할 때 사용한다. 내부적으로 JsonObjectMarshaller를 사용하여 자바 객체를 텍스트로 전환한다.

💻 실전 코드: JSON 결과 보고서 작성

@Bean
@StepScope
public JsonFileItemWriter<DeathNote> deathNoteJsonWriter(
        @Value("#{jobParameters['outputPath']}") String outputPath) {
    return new JsonFileItemWriterBuilder<DeathNote>()
            .name("deathNoteJsonWriter")
            .resource(new FileSystemResource(outputPath + "/death_notes.json"))
            // [핵심] 마샬러 장착 (객체를 JSON 문자열로 변환)
            .jsonObjectMarshaller(new JacksonJsonObjectMarshaller<>())
            .build();
}
  • 결과물 구조: 실행 결과는 각 객체 사이에 쉼표가 붙고 전체가 []로 감싸진 완벽한 JSON 배열 형식이 된다.
  • 주의: 만약 결과물을 다시 JSONL(한 줄에 객체 하나) 형식으로 쓰고 싶다면, 이전 작전에서 배운 FlatFileItemWriter에 ObjectMapper를 사용하는 커스텀 LineAggregator 방식을 사용해야 한다.

 

 

6. 마무리하며

이번 작전을 통해 스프링 배치가 JSON이라는 유연한 형식을 어떻게 정복하는지 살펴보았다.

  1. JSONL 전략: 성능과 단순함이 중요하다면 FlatFileItemReader + Custom LineMapper.
  2. 구조 분석: 여러 줄로 쪼개진 데이터는 JsonRecordSeparatorPolicy로 통합.
  3. 표준 배열 처리: 정형화된 JSON 배열은 JsonItemReader와 JsonFileItemWriter로 대응.

이제 파일 시스템에서 일어나는 모든 형태의 데이터 입출력(Flat File, CSV, Fixed-Length, JSON)을 알아보았다.

반응형