Spring Boot와 Jackson
Spring Boot는 JSON 직렬화/역직렬화에 Jackson을 기본으로 사용한다. @RestController의 응답, @RequestBody의 요청 파싱, RestClient/WebClient의 HTTP 통신 모두 Jackson의 ObjectMapper를 거친다. 기본 설정만으로도 대부분 동작하지만, 실무에서는 날짜 포맷, null 처리, 다형성 직렬화 등 세밀한 커스터마이징이 필수다.
전역 ObjectMapper 설정
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> builder
// snake_case ↔ camelCase 자동 변환
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
// null 필드 제외
.serializationInclusion(JsonInclude.Include.NON_NULL)
// 알 수 없는 필드 무시 (API 버전 호환)
.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
// 빈 객체 직렬화 허용
.featuresToDisable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
// 날짜를 timestamp 대신 ISO 8601로
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
// Java 8 날짜/시간 모듈
.modules(new JavaTimeModule());
}
}
// 또는 application.yml로 설정
// spring:
// jackson:
// property-naming-strategy: SNAKE_CASE
// default-property-inclusion: non_null
// deserialization:
// fail-on-unknown-properties: false
// serialization:
// write-dates-as-timestamps: false
Jackson2ObjectMapperBuilderCustomizer를 사용하면 Spring Boot의 자동 설정을 유지하면서 추가 커스터마이징할 수 있다. ObjectMapper Bean을 직접 등록하면 자동 설정이 전부 무시되므로 주의해야 한다.
필드 레벨 어노테이션
public record UserResponse(
Long id,
@JsonProperty("user_name") // JSON 키 이름 변경
String name,
@JsonIgnore // 직렬화/역직렬화 모두 제외
String password,
@JsonProperty(access = JsonProperty.Access.READ_ONLY) // 응답에만 포함
LocalDateTime createdAt,
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) // 요청에서만 수용
String secretToken,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
LocalDateTime updatedAt,
@JsonInclude(JsonInclude.Include.NON_EMPTY) // 빈 리스트 제외
List<String> roles
) {}
// 조건부 직렬화
public class OrderDto {
@JsonInclude(value = JsonInclude.Include.CUSTOM,
valueFilter = SensitiveFilter.class)
private String cardNumber;
// 필터: 값이 민감 데이터면 제외
public static class SensitiveFilter {
@Override
public boolean equals(Object obj) {
return obj instanceof String s && s.length() > 10;
}
}
}
커스텀 Serializer / Deserializer
// 금액을 "₩10,000" 형식으로 직렬화
public class MoneySerializer extends JsonSerializer<BigDecimal> {
private static final DecimalFormat FORMAT = new DecimalFormat("#,##0");
@Override
public void serialize(BigDecimal value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
gen.writeString("₩" + FORMAT.format(value));
}
}
// "₩10,000" → BigDecimal 역직렬화
public class MoneyDeserializer extends JsonDeserializer<BigDecimal> {
@Override
public BigDecimal deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
String text = p.getText().replaceAll("[₩,]", "");
return new BigDecimal(text);
}
}
// 적용 방법 1: 필드에 직접
public record ProductResponse(
String name,
@JsonSerialize(using = MoneySerializer.class)
@JsonDeserialize(using = MoneyDeserializer.class)
BigDecimal price
) {}
// 적용 방법 2: 모듈로 전역 등록
@Configuration
public class JacksonModuleConfig {
@Bean
public Module moneyModule() {
SimpleModule module = new SimpleModule("MoneyModule");
module.addSerializer(Money.class, new MoneySerializer());
module.addDeserializer(Money.class, new MoneyDeserializer());
return module;
}
}
@JsonView: 동일 DTO, 다른 뷰
// 뷰 정의
public class Views {
public interface Summary {}
public interface Detail extends Summary {}
public interface Admin extends Detail {}
}
// DTO
public record UserDto(
@JsonView(Views.Summary.class) Long id,
@JsonView(Views.Summary.class) String name,
@JsonView(Views.Detail.class) String email,
@JsonView(Views.Detail.class) LocalDateTime createdAt,
@JsonView(Views.Admin.class) String role,
@JsonView(Views.Admin.class) boolean active
) {}
// 컨트롤러
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
@JsonView(Views.Summary.class) // id, name만 반환
public List<UserDto> listUsers() { ... }
@GetMapping("/{id}")
@JsonView(Views.Detail.class) // id, name, email, createdAt 반환
public UserDto getUser(@PathVariable Long id) { ... }
@GetMapping("/{id}/admin")
@JsonView(Views.Admin.class) // 모든 필드 반환
@PreAuthorize("hasRole('ADMIN')")
public UserDto getUserAdmin(@PathVariable Long id) { ... }
}
같은 DTO를 엔드포인트마다 다른 형태로 노출할 수 있다. 별도 Response DTO를 만들지 않아도 되므로 Spring ArgumentResolver와 함께 사용하면 코드 중복을 크게 줄일 수 있다.
다형성 직렬화: @JsonTypeInfo
// 이벤트 상속 계층
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OrderCreatedEvent.class, name = "order_created"),
@JsonSubTypes.Type(value = OrderShippedEvent.class, name = "order_shipped"),
@JsonSubTypes.Type(value = OrderCancelledEvent.class, name = "order_cancelled"),
})
public sealed interface OrderEvent permits
OrderCreatedEvent, OrderShippedEvent, OrderCancelledEvent {
Long orderId();
Instant occurredAt();
}
public record OrderCreatedEvent(
Long orderId, Instant occurredAt,
BigDecimal totalAmount, List<String> items
) implements OrderEvent {}
public record OrderShippedEvent(
Long orderId, Instant occurredAt,
String trackingNumber, String carrier
) implements OrderEvent {}
public record OrderCancelledEvent(
Long orderId, Instant occurredAt,
String reason
) implements OrderEvent {}
// JSON 결과:
// {"type":"order_created","order_id":1,"total_amount":"₩50,000",...}
// {"type":"order_shipped","order_id":1,"tracking_number":"ABC123",...}
// 역직렬화: type 필드로 자동 타입 결정
@PostMapping("/events")
public void handleEvent(@RequestBody OrderEvent event) {
switch (event) {
case OrderCreatedEvent e -> processCreated(e);
case OrderShippedEvent e -> processShipped(e);
case OrderCancelledEvent e -> processCancelled(e);
}
}
Mixin: 외부 클래스 직렬화 제어
// 외부 라이브러리 클래스에 Jackson 어노테이션을 입힐 수 없을 때
// → Mixin으로 해결
@JsonIgnoreProperties({"internalState", "debugInfo"})
public abstract class ExternalDtoMixin {
@JsonProperty("display_name")
abstract String getName();
@JsonFormat(pattern = "yyyy-MM-dd")
abstract LocalDate getBirthDate();
}
@Configuration
public class JacksonMixinConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer mixinCustomizer() {
return builder -> builder.mixIn(ExternalDto.class, ExternalDtoMixin.class);
}
}
성능 최적화
@Configuration
public class JacksonPerformanceConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer performanceCustomizer() {
return builder -> builder
// Afterburner: 리플렉션 대신 바이트코드 생성
.modules(new AfterburnerModule())
// 또는 Blackbird (Java 11+, Afterburner 후속)
// .modules(new BlackbirdModule())
// 스트리밍 API로 대용량 JSON 처리
.featuresToEnable(JsonGenerator.Feature.AUTO_CLOSE_TARGET)
// 날짜 캐싱
.featuresToEnable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
}
}
| 최적화 | 효과 | 적용 |
|---|---|---|
| Afterburner/Blackbird | 직렬화 20-30% 성능 향상 | jackson-module-afterburner 의존성 추가 |
| ObjectMapper 재사용 | 인스턴스 생성 비용 제거 | Bean으로 싱글톤 관리 (Spring 기본) |
| NON_NULL 포함 | JSON 크기 감소, 네트워크 절약 | 전역 serializationInclusion 설정 |
| @JsonView | 불필요한 필드 직렬화 방지 | 엔드포인트별 뷰 적용 |
Jackson은 Spring의 보이지 않는 심장이다. Spring RestClient의 HTTP 통신부터 API 응답까지 모든 JSON 처리를 담당하므로, 전역 설정을 올바르게 잡고 커스텀 Serializer/Mixin을 적재적소에 활용하는 것이 API 품질을 좌우한다.