Jackson은 Java 진영에서 가장 널리 채택되는 JSON 처리 엔진으로, FasterXML이 관리하고 있다. 단순한 JSON 파싱을 넘어 XML, YAML, CSV 등 다양한 포맷을 지원하며, 뛰어난 성능과 유연한 확장성으로 Spring 생태계의 사실상 표준이 되었다. 이번 글에서는 Jackson의 내부 메커니즘과 실무 활용법을 살펴본다.
핵심 모듈 구성
| 모듈명 | Maven Artifact | 역할 |
|---|---|---|
jackson-core |
com.fasterxml.jackson.core:jackson-core |
저수준 스트리밍 API (JsonParser, JsonGenerator) 제공 |
jackson-databind |
com.fasterxml.jackson.core:jackson-databind |
고수준 데이터 바인딩 (ObjectMapper) |
jackson-annotations |
com.fasterxml.jackson.core:jackson-annotations |
필드/메서드 제어용 어노테이션 |
Spring Boot 프로젝트라면 starter 의존성에 기본 내장되어 별도 추가가 불필요하다.
내부 동작 아키텍처
[Java 객체]
↓ (ObjectMapper)
[토큰 스트림] ←→ [JSON 텍스트]
↓ (JsonParser / JsonGenerator)
[바이트 스트림 / Reader / Writer]
ObjectMapper: 모든 작업의 중추. 직렬화(writeValue)와 역직렬화(readValue)를 담당JsonFactory: 파서와 생성기의 팩토리JsonParser: JSON 문자열을 토큰 단위로 분해 (START_OBJECT,FIELD_NAME,VALUE_NUMBER_INT등)JsonGenerator: 토큰 스트림을 목적지로 기록
Jackson의 성능 우위는 DOM 트리를 구축하지 않고 스트리밍 방식 + 바이트코드 생성으로 즉시 변환하기 때문이다. 이는 Gson 등과 비교했을 때 메모리 오버헤드가 현저히 적다.
직렬화/역직렬화 흐름
직렬화 (Java → JSON)
ObjectMapper.writeValue()호출- 리플렉션/ASM으로 객체의 필드·메서드 메타데이터 수집
- 어노테이션 규칙에 따라 필드 필터링 및 이름 매핑
- 토큰 스트림 생성 →
JsonGenerator통해 최종 출력
역직렬화 (JSON → Java)
ObjectMapper.readValue()호출JsonParser가 JSON을 토큰 스트림으로 해석- 대상 타입의 인스턴스 생성 (기본 생성자 또는
@JsonCreator지정 생성자) - 필드명과 토큰 매칭 후 setter 또는 직접 필드 주입
기본 사용법
POJO 변환
public class Member {
public String nickname;
private int score;
// 기본 생성자 필수 (역직렬화 시)
public Member() {}
public Member(String nickname, int score) {
this.nickname = nickname;
this.score = score;
}
public int getScore() { return score; }
public void setScore(int score) { this.score = score; }
}
// ObjectMapper 인스턴스 생성
ObjectMapper om = new ObjectMapper();
// 객체 → JSON 문자열
Member member = new Member("kimi", 150);
String jsonText = om.writeValueAsString(member);
// 결과: {"nickname":"kimi","score":150}
// JSON 문자열 → 객체
Member parsed = om.readValue(jsonText, Member.class);
역직렬화 조건: 필드가
public이거나 getter/setter가 있어야 하며, 인자 없는 생성자가 존재해야 한다.
어노테이션 활용
| 어노테이션 | 용도 | 적용 예시 |
|---|---|---|
@JsonProperty |
JSON 속성명 재정의 | @JsonProperty("user_nick") String nickname; |
@JsonIgnore |
필드 완전 제외 | @JsonIgnore String internalToken; |
@JsonFormat |
날짜/수치 포맷 지정 | @JsonFormat(pattern="yyyy-MM-dd") LocalDate joined; |
@JsonInclude |
null/빈값 출력 제어 | @JsonInclude(JsonInclude.Include.NON_EMPTY) |
@JsonCreator |
커스텀 생성자 지정 | 생성자에 직접 부착 |
public class Item {
@JsonProperty("item_no")
private Long itemId;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String remark; // null인 경우 JSON에서 생략
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy/MM/dd HH:mm")
private LocalDateTime registeredAt;
@JsonIgnore
private String apiSecret;
// getter, setter...
}
복잡 시나리오 대응
커스텀 생성자를 통한 역직렬화
불변 객체를 안전하게 복원하려면 @JsonCreator를 활용한다.
public class Coordinate {
private final double latitude;
private final double longitude;
@JsonCreator
public Coordinate(
@JsonProperty("lat") double latitude,
@JsonProperty("lng") double longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
}
// 입력: {"lat":37.5665,"lng":126.9780} → Coordinate 객체 생성
제네릭 컬렉션 처리
// 컴파일 시 타입 소거를 우회하기 위해 TypeReference 사용
Set<Member> memberSet = om.readValue(jsonData, new TypeReference<Set<Member>>() {});
알 수 없는 필드 무시
om.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
사용자 정의 직렬화 로직
public class PercentageSerializer extends JsonSerializer<BigDecimal> {
@Override
public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider prov)
throws IOException {
gen.writeString(value.multiply(BigDecimal.valueOf(100))
.setScale(2, RoundingMode.HALF_UP) + "%");
}
}
// 적용
public class Ratio {
@JsonSerialize(using = PercentageSerializer.class)
private BigDecimal rate; // 0.1567 → "15.67%"
}
Spring Boot 전역 설정
@Configuration
public class JacksonGlobalConfig {
@Bean
@Primary
public ObjectMapper customMapper() {
ObjectMapper mapper = new ObjectMapper();
// 알 수 없는 필드 무시
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// 빈 객체 예외 방지
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
// 날짜/시간 문자열로 출력
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.registerModule(new JavaTimeModule());
// null 속성 미출력
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 들여쓰기 포맷팅 (개발 환경)
mapper.enable(SerializationFeature.INDENT_OUTPUT);
return mapper;
}
}
YAML 기반 설정도 가능하다:
spring:
jackson:
default-property-inclusion: non_null
serialization:
write-dates-as-timestamps: false
indent-output: true
deserialization:
fail-on-unknown-properties: false
고급 기법
MixIn 패턴
소스 수정이 불가한 외부 클래스에 마치 어노테이션을 붙인 것 같은 효과를 준다.
public abstract class LocalDateTimeMixin {
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@JsonSerialize(using = LocalDateTimeSerializer.class)
public abstract void dummy(LocalDateTime dt);
}
// 등록
mapper.addMixIn(LocalDateTime.class, LocalDateTimeMixin.class);
JsonNode 트리 탐색
POJO 정의 없이 동적으로 JSON을 탐색해야 할 때 유용하다.
JsonNode root = om.readTree(jsonString);
if (root.has("metadata")) {
String version = root.path("metadata").path("version").asText("1.0");
JsonNode tags = root.at("/metadata/tags"); // JSON Pointer 문법
}
대용량 파일 스트리밍
메모리에 전체를 로드하지 않고 이벤트 기반으로 처리한다.
try (JsonParser jp = om.createParser(new File("massive-data.json"))) {
JsonToken token;
while ((token = jp.nextToken()) != null) {
if (token == JsonToken.FIELD_NAME && "records".equals(jp.currentName())) {
jp.nextToken(); // START_ARRAY
while (jp.nextToken() == JsonToken.START_OBJECT) {
// 개별 레코드를 Map 또는 POJO로 변환
Map<String, Object> record = jp.readValueAs(HashMap.class);
processRecord(record);
}
}
}
}
문제 해결 가이드
| 증상 | 원인 | 대응책 |
|---|---|---|
| 순환 참조 StackOverflow | 양방향 연관관계 | @JsonIdentityInfo 적용 또는 DTO 분리 |
| 날짜 형식 불일치 | 기본 ISO-8601 vs 커스텀 포맷 | @JsonFormat 또는 DateTimeFormatter 등록 |
| 역직렬화 실패 | 예상치 못한 필드 존재 | FAIL_ON_UNKNOWN_PROPERTIES 비활성화 |
| 성능 저하 | ObjectMapper 반복 생성 | 싱글톤으로 재사용 (스레드 안전) |
| 한글 깨짐 | 출력 스트림 인코딩 미지정 | application/json; charset=UTF-8 명시 |
ObjectMapper는 생성 비용이 크므로 애플리케이션 수명 주기 동안 재사용하라. 해당 클래스는 스레드 안전하다.
라이브러리 비교
| 특성 | Jackson | Gson | Fastjson |
|---|---|---|---|
| 처리 속도 | 매우 빠름 | 보통 | 빠르나 안정성 이슈 |
| 어노테이션 지원 | 풍부함 | 제한적 | 중간 |
| Spring 통합 | 기본 내장 | 교체 필요 | 교체 필요 |
| 보안 이력 | 양호 | 양호 | 취약점 다수 |
| 스트리밍 API | 지원 | 미지원 | 지원 |
Spring 기반 프로젝트에서는 Jackson이 생태계 통합도와 안정성 면에서 압도적이다.
핵 정리
Jackson의 설계 철학은 계층적 분리에 있다. 스트리밍 엔진을 기반으로 데이터 바인딩과 어노테이션 처리를 올려, 단순한 사용부터 고성능 처리까지 모두 커버한다. ObjectMapper의 싱글톤 관리, 적절한 어노테이션 활용, 그리고 필요시 커스텀 시리얼라이저 확장이 실무에서의 핵심 패턴이다.