Spring Batch 입문 32편. 모던 batch 의 표준 포맷 — JSON 처리. JsonItemReader · JsonFileItemWriter, JsonObjectReader 인터페이스 (Jackson · Gson 구현), JSON array 입력 가정, JsonObjectMarshaller 출력 직렬화, NDJSON 같은 변형 처리까지 정리한 학습 노트.
이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 32편이에요. 31편 의 XML 처리에서 한 발 더 — 모던 batch 의 가장 흔한 포맷 JSON 처리.
Spring Batch 의 JSON 입력 가정
입력 JSON resource 가 JSON object 의 array 라고 가정. — 공식 reference
[
{
"isin": "123",
"quantity": 1,
"price": 1.2,
"customer": "foo"
},
{
"isin": "456",
"quantity": 2,
"price": 1.4,
"customer": "bar"
}
]
→ 최상위 = JSON array [...], 각 원소 = JSON object = item 1개.
NDJSON (newline-delimited JSON, JSON Lines) 같은 변형은 기본 지원 X — 별도 처리.
JsonItemReader 인터페이스
@Bean
public JsonItemReader<Trade> jsonReader() {
return new JsonItemReaderBuilder<Trade>()
.name("tradeJsonReader")
.resource(new ClassPathResource("trades.json"))
.jsonObjectReader(new JacksonJsonObjectReader<>(Trade.class))
.build();
}
필수는 둘. resource 가 입력 JSON 파일을 가리키고, jsonObjectReader 가 JSON 파싱·매핑을 JsonObjectReader 인터페이스에 위임한다.
JsonObjectReader — JSON 파싱 추상화
public interface JsonObjectReader<T> {
void open(Resource resource) throws Exception;
T read() throws Exception;
void close() throws IOException;
}
Spring Batch 가 특정 JSON 라이브러리 강제하지 않음 — 인터페이스만 만족하면 OK.
표준 구현 2종
| 구현 | 라이브러리 |
|---|---|
JacksonJsonObjectReader<T> |
Jackson — com.fasterxml.jackson.databind |
GsonJsonObjectReader<T> |
Gson — com.google.gson |
대부분 환경 = Jackson. Spring Boot 가 기본 포함.
JacksonJsonObjectReader 사용
@Bean
public JsonItemReader<Trade> jacksonReader() {
return new JsonItemReaderBuilder<Trade>()
.name("jacksonTradeReader")
.resource(new ClassPathResource("trades.json"))
.jsonObjectReader(new JacksonJsonObjectReader<>(Trade.class))
.build();
}
생성자 인자 = target type (도메인 class). Trade.class 지정 = Jackson 이 각 JSON object → Trade 매핑.
GsonJsonObjectReader 사용
@Bean
public JsonItemReader<Trade> gsonReader() {
return new JsonItemReaderBuilder<Trade>()
.name("gsonTradeReader")
.resource(new ClassPathResource("trades.json"))
.jsonObjectReader(new GsonJsonObjectReader<>(Trade.class))
.build();
}
Jackson vs Gson 선택 — 환경 의존성 + 팀 표준 으로 결정. 두 라이브러리 기능적 차이 거의 없음.
Streaming Parsing 의 중요성
JsonObjectReader 의 핵심:
streaming API 를 사용해 chunk 단위로 JSON object 를 read — 공식 reference
전체 JSON 을 메모리에 적재하지 않고 array 의 원소를 하나씩 추출.
→ XML 의 StAX (Streaming API for XML, 이벤트 기반 XML 파서) 와 같은 원리. 대용량 JSON 도 메모리 O(1) 처리.
Jackson JsonParser·Gson JsonReader 가 low-level streaming API. JacksonJsonObjectReader·GsonJsonObjectReader 가 그 위 wrapping.
도메인 예제 — Trade
Jackson annotation
public class Trade {
@JsonProperty("isin")
private String isin;
@JsonProperty("quantity")
private long quantity;
@JsonProperty("price")
private BigDecimal price;
@JsonProperty("customer")
private String customer;
// getter/setter
}
대부분 field 이름 = JSON key 일치 시 @JsonProperty 생략 가능. Jackson 이 snake_case ↔ camelCase 도 명시 설정 시 자동 처리.
Java Record + Jackson
public record Trade(
String isin,
long quantity,
BigDecimal price,
String customer
) {}
Jackson 2.12+ = Record 자동 인식. setter 불필요. 모던 권장.
JsonFileItemWriter
@Bean
public JsonFileItemWriter<Trade> jsonWriter() {
return new JsonFileItemWriterBuilder<Trade>()
.name("tradeJsonWriter")
.resource(new FileSystemResource("trades-out.json"))
.jsonObjectMarshaller(new JacksonJsonObjectMarshaller<>())
.build();
}
여기도 필수는 둘. resource 가 출력 JSON 파일이고, jsonObjectMarshaller 가 Java → JSON 변환을 담당한다.
JsonObjectMarshaller — 출력 추상화
public interface JsonObjectMarshaller<T> {
String marshal(T item);
}
각 item → JSON String 변환.
표준 구현 2종
| 구현 | 라이브러리 |
|---|---|
JacksonJsonObjectMarshaller<T> |
Jackson |
GsonJsonObjectMarshaller<T> |
Gson |
출력 구조
[
{"isin":"123","quantity":1,"price":1.2,"customer":"foo"},
{"isin":"456","quantity":2,"price":1.4,"customer":"bar"}
]
JsonFileItemWriter 가:
- 파일 시작 —
[작성 - 각 item — Marshaller 로 JSON string 생성 + comma 구분
- 파일 끝 —
]작성
표준 JSON array 구조 유지.
옵션
@Bean
public JsonFileItemWriter<Trade> tradeWriter() {
return new JsonFileItemWriterBuilder<Trade>()
.name("tradeWriter")
.resource(new FileSystemResource("trades.json"))
.jsonObjectMarshaller(new JacksonJsonObjectMarshaller<>())
.encoding("UTF-8")
.lineSeparator("\n")
.append(false)
.shouldDeleteIfEmpty(true)
.shouldDeleteIfExists(false)
.transactional(true)
.headerCallback(w -> w.write("/* header comment */"))
.footerCallback(w -> w.write("/* footer */"))
.build();
}
FlatFileItemWriter (30편) 와 동일 옵션 다수.
End-to-end 예제
Reader
@Bean
@StepScope
public JsonItemReader<Trade> jsonReader(
@Value("#{jobParameters['input.file']}") Resource resource) {
return new JsonItemReaderBuilder<Trade>()
.name("jsonReader")
.resource(resource)
.jsonObjectReader(new JacksonJsonObjectReader<>(Trade.class))
.build();
}
Writer
@Bean
@StepScope
public JsonFileItemWriter<Trade> jsonWriter(
@Value("#{jobParameters['output.file']}") Resource resource) {
return new JsonFileItemWriterBuilder<Trade>()
.name("jsonWriter")
.resource(resource)
.jsonObjectMarshaller(new JacksonJsonObjectMarshaller<>())
.build();
}
Step
@Bean
public Step jsonStep(JobRepository repo, PlatformTransactionManager tx,
JsonItemReader<Trade> reader,
JsonFileItemWriter<Trade> writer) {
return new StepBuilder("jsonStep", repo)
.<Trade, Trade>chunk(100, tx)
.reader(reader)
.writer(writer)
.build();
}
JSON → JSON pipeline 완성.
NDJSON 처리 — 직접 구현
NDJSON (Newline-Delimited JSON) = 각 line 이 독립 JSON object:
{"isin":"123","quantity":1}
{"isin":"456","quantity":2}
{"isin":"789","quantity":3}
Spring Batch 표준 지원 X — 직접 구현 또는 외부 라이브러리.
방법 1: FlatFileItemReader + Jackson
@Bean
public FlatFileItemReader<Trade> ndjsonReader() {
return new FlatFileItemReaderBuilder<Trade>()
.name("ndjsonReader")
.resource(new FileSystemResource("trades.ndjson"))
.lineMapper((line, lineNumber) ->
objectMapper.readValue(line, Trade.class))
.build();
}
FlatFileItemReader 가 line 단위 read + custom LineMapper 가 각 line 을 Jackson 으로 parse.
방법 2: Custom JsonObjectReader
26편 Custom Reader 패턴 — JsonObjectReader 직접 구현 + NDJSON 의 line 기반 streaming.
Jackson 추가 설정
ObjectMapper customization
기본 JacksonJsonObjectReader 의 ObjectMapper (Jackson 의 JSON ↔ Java 변환기) 가 기본 설정. custom ObjectMapper 사용:
@Bean
public ObjectMapper customMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule()) // LocalDate 등
.registerModule(new ParameterNamesModule()) // Record 강화
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Bean
public JacksonJsonObjectReader<Trade> jsonReader(ObjectMapper mapper) {
return new JacksonJsonObjectReader<>(mapper, Trade.class);
}
FAIL_ON_UNKNOWN_PROPERTIES = false = 알 수 없는 필드 무시. 운영 batch 권장 (스키마 변경 흡수).
LocalDate·LocalDateTime
{"orderDate": "2026-05-17"}
Java LocalDate 매핑 = JavaTimeModule (Jackson 의 Java 8 시간 타입 지원 모듈) 등록 필수. Spring Boot 가 자동 등록 — Jackson2ObjectMapperBuilder 활용 시.
snake_case ↔ camelCase
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
JSON "order_date" ↔ Java orderDate 자동 변환.
자주 만나는 사고
사고 1: 최상위 array 가 아닌 단일 object
원인 — JSON 파일이 [{...}, {...}] 가 아닌 {...} (단일 object).
해결 — JSON 을 array 로 wrap 후 read, 또는 custom JsonObjectReader.
사고 2: Unknown property exception
원인 — Jackson 의 FAIL_ON_UNKNOWN_PROPERTIES = true (default).
해결 — Custom ObjectMapper 로 false 설정.
사고 3: Date format mismatch
원인 — JSON "2026-05-17T10:30:00" 가 Jackson 의 기본 ISO 처리 안 맞음.
해결 — @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "...") 또는 ObjectMapper config.
사고 4: Record 인식 안 됨
원인 — Jackson 버전 2.12 미만.
해결 — Jackson 2.12+ + ParameterNamesModule (생성자 파라미터 이름 기반 매핑 모듈) 등록. Spring Boot 3+ = 자동.
사고 5: BigDecimal 정밀도 손실
원인 — JSON 1.2 가 Java double 으로 parse 후 BigDecimal 변환 → 정밀도 손실.
해결 — mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS).
사고 6: 출력 JSON 의 indent 없음
원인 — JacksonJsonObjectMarshaller 기본 = compact.
해결 — Custom ObjectMapper + mapper.enable(SerializationFeature.INDENT_OUTPUT).
사고 7: NDJSON 을 표준 JsonItemReader 로 시도
원인 — NDJSON 은 array 아닌 line-based.
해결 — FlatFileItemReader + custom LineMapper 또는 custom JsonObjectReader.
사고 8: 출력 array 가 append 모드 에서 깨짐
원인 — append(true) + 두 번째 실행 → ][ 가 중간에 생김.
해결 — append(false) + shouldDeleteIfExists(true). 또는 NDJSON 으로 전환.
운영 권장 패턴
Pattern 1: 표준 Jackson reader
@Bean
@StepScope
public JsonItemReader<Trade> tradeReader(
@Value("#{jobParameters['input.file']}") Resource resource,
ObjectMapper objectMapper) {
return new JsonItemReaderBuilder<Trade>()
.name("tradeReader")
.resource(resource)
.jsonObjectReader(new JacksonJsonObjectReader<>(objectMapper, Trade.class))
.build();
}
Spring Boot 의 기본 ObjectMapper 재사용 — JavaTimeModule 자동 등록.
Pattern 2: NDJSON reader
@Bean
@StepScope
public FlatFileItemReader<Trade> ndjsonReader(
@Value("#{jobParameters['input.file']}") Resource resource,
ObjectMapper mapper) {
return new FlatFileItemReaderBuilder<Trade>()
.name("ndjsonReader")
.resource(resource)
.lineMapper((line, lineNumber) -> mapper.readValue(line, Trade.class))
.build();
}
NDJSON 처리 표준 패턴. 대용량 stream 친화.
Pattern 3: XML → JSON 변환 batch
@Bean
public Step xmlToJsonStep(JobRepository repo, PlatformTransactionManager tx,
StaxEventItemReader<Trade> xmlReader,
JsonFileItemWriter<Trade> jsonWriter) {
return new StepBuilder("xmlToJson", repo)
.<Trade, Trade>chunk(100, tx)
.reader(xmlReader)
.writer(jsonWriter)
.build();
}
XML 원본 → JSON 출력. format 변환 batch.
Pattern 4: JSON → DB
@Bean
public Step jsonToDbStep(JobRepository repo, PlatformTransactionManager tx,
JsonItemReader<Trade> jsonReader,
JdbcBatchItemWriter<Trade> dbWriter) {
return new StepBuilder("jsonToDb", repo)
.<Trade, Trade>chunk(500, tx)
.reader(jsonReader)
.writer(dbWriter)
.build();
}
JSON dump → DB 적재. 외부 시스템 동기화 패턴.
Pattern 5: Indent + 가독성 출력
@Bean
public ObjectMapper prettyMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.enable(SerializationFeature.INDENT_OUTPUT)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
@Bean
public JsonFileItemWriter<Trade> prettyWriter(ObjectMapper mapper) {
return new JsonFileItemWriterBuilder<Trade>()
.name("prettyWriter")
.resource(new FileSystemResource("trades-pretty.json"))
.jsonObjectMarshaller(new JacksonJsonObjectMarshaller<>(mapper))
.build();
}
운영자 검토용 가독성 출력. 성능보다 사람이 읽기 좋게.
시험 직전 한 번 더 — JSON Reader/Writer 함정 압축 노트
- 입력 가정 = JSON object 의 array (
[{...}, {...}]) - NDJSON = 표준 지원 X (FlatFileItemReader + custom LineMapper 로 처리)
- JsonItemReader 2가지 필수 = resource · jsonObjectReader
- JsonFileItemWriter 2가지 필수 = resource · jsonObjectMarshaller
- JsonObjectReader 인터페이스 =
open·read·close(ItemStream 유사) - JsonObjectMarshaller 인터페이스 =
marshal(item)→ JSON String - 표준 구현 2종 = Jackson (JacksonJsonObjectReader · JacksonJsonObjectMarshaller) · Gson (GsonJsonObjectReader · GsonJsonObjectMarshaller)
- Jackson = 대부분 환경 (Spring Boot 기본)
- streaming API = Jackson
JsonParser· GsonJsonReader— 메모리 O(1) - target type =
new JacksonJsonObjectReader<>(Trade.class) - custom ObjectMapper =
new JacksonJsonObjectReader<>(mapper, Trade.class) - Java Record = Jackson 2.12+ 자동 인식
- 출력 구조 =
[{...},{...}]표준 array - 옵션 — encoding · lineSeparator · append · shouldDeleteIfEmpty · shouldDeleteIfExists · transactional · headerCallback · footerCallback (FlatFile 과 동일)
- 함정 — 단일 object 입력 (array 가 아님) → custom reader
- 함정 — Unknown property →
FAIL_ON_UNKNOWN_PROPERTIES = false - 함정 — LocalDate ↔
JavaTimeModule등록 필수 - 함정 —
BigDecimal정밀도 →USE_BIG_DECIMAL_FOR_FLOATS - 함정 — indent 없음 →
INDENT_OUTPUT - 함정 — append + array 가
][깨짐 → append 금지 또는 NDJSON - snake_case ↔ camelCase =
PropertyNamingStrategies.SNAKE_CASE - 패턴 — 표준 Jackson + Spring Boot ObjectMapper 재사용
- 패턴 — NDJSON = FlatFileItemReader + custom LineMapper
- 패턴 — XML → JSON / JSON → DB 변환 batch
- 패턴 — pretty output (INDENT_OUTPUT)
공식 문서: JSON Item Readers And Writers 에서 원문을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 27편 — Flat File Overview · 파싱 3총사
- 28편 — FieldSet · Flat File 의 ResultSet
- 29편 — FlatFileItemReader 깊은 옵션
- 30편 — FlatFileItemWriter · LineAggregator · FieldExtractor
- 31편 — XML Reader · Writer · StAX 기반 streaming
다음 글: