기술과 산업/언어 및 프레임워크

Java JSON 처리 실전 시리즈 8화 – 커스텀 Serializer와 Deserializer로 JSON 변환 제어하기

B컷개발자 2025. 5. 19. 10:57
728x90

Jackson에서 커스텀 Serializer 및 Deserializer를 구현해 특수 포맷, 민감 정보, 복합 필드에 대한 JSON 직렬화/역직렬화를 제어하는 방법을 실무 예제 중심으로 설명합니다.


Spring Boot에서 기본적인 JSON 직렬화/역직렬화는 ObjectMapper가 자동으로 처리해줍니다. 그러나 아래와 같은 상황에서는 커스텀 Serializer 또는 Deserializer가 필요해집니다.

  • 특정 필드를 Base64 인코딩 또는 마스킹 처리하고 싶을 때
  • 날짜 포맷이 비표준이거나 복합적인 형태일 때
  • 중첩된 객체를 평탄화하거나 축약해서 표현하고 싶을 때
  • 클라이언트에 따라 JSON 포맷을 맞춤화하고 싶을 때

이번 글에서는 Jackson에서 커스텀 직렬화/역직렬화 클래스를 구현하고 적용하는 방법을 구체적인 예제와 함께 설명합니다.


1. 기본 구조: 커스텀 Serializer / Deserializer 클래스

커스텀 Serializer

public class MaskingSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString("***" + value.substring(value.length() - 3));
    }
}

커스텀 Deserializer

public class EmailDeserializer extends JsonDeserializer<String> {
    @Override
    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String raw = p.getText();
        return raw.toLowerCase().trim();
    }
}

2. DTO 필드에 적용하기

public class User {

    private String name;

    @JsonSerialize(using = MaskingSerializer.class)
    @JsonDeserialize(using = EmailDeserializer.class)
    private String email;
}

요청 JSON

{
  "name": "홍길동",
  "email": "ADMIN@EXAMPLE.COM  "
}

역직렬화 후 객체 상태

user.getEmail(); // "admin@example.com"

직렬화된 JSON 응답

{
  "name": "홍길동",
  "email": "***com"
}

3. 커스텀 Serializer를 전역으로 등록하기

만약 특정 타입 전체에 대해 동일한 커스터마이징을 적용하고 싶다면 SimpleModule을 통해 전역 등록이 가능합니다.

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();

    SimpleModule module = new SimpleModule();
    module.addSerializer(String.class, new MaskingSerializer());
    mapper.registerModule(module);

    return mapper;
}

주의: String 전체에 마스킹을 적용하면 모든 문자열 필드에 동일한 로직이 적용되므로 주의가 필요합니다. 특정 DTO나 조건부로 제한하는 것이 바람직합니다.


4. 실무 응용 예시

(1) 주민등록번호 마스킹

public class RrnMaskingSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(String rrn, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString(rrn.replaceAll("^\\d{6}-\\d{1}", "******-*"));
    }
}

(2) 금액 단위 포맷 처리 (예: 1,000,000 → 100만)

public class AmountShortenSerializer extends JsonSerializer<Long> {
    @Override
    public void serialize(Long amount, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (amount >= 1_000_000) {
            gen.writeString((amount / 1_000_000) + "만");
        } else {
            gen.writeNumber(amount);
        }
    }
}

5. Deserializer에서 Enum 커스텀 매핑

public enum Role {
    ADMIN, USER;
}

public class RoleDeserializer extends JsonDeserializer<Role> {
    @Override
    public Role deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String value = p.getText().toUpperCase(Locale.ROOT);
        try {
            return Role.valueOf(value);
        } catch (Exception e) {
            return Role.USER;
        }
    }
}

이 방식은 잘못된 값이 들어올 때 안전하게 기본값으로 매핑해주는 데 유용합니다.


6. 유지보수 팁과 주의사항

  • 커스텀 Serializer/Deserializer는 직관성이 떨어지므로 꼭 필요한 경우에만 사용
  • 테스트 코드를 반드시 작성해 동작을 검증
  • 불변 객체(record, Lombok 등)에서는 생성자 기반 역직렬화 설정을 함께 고려해야 함
  • Jackson 버전이 낮으면 일부 동작이 제한될 수 있으므로 최소 2.10 이상 권장

다음 회차 예고

9화에서는 실무에서 종종 마주하는 문제 중 하나인 따옴표 없는 비표준 JSON 처리에 대해 다룹니다. ALLOW_UNQUOTED_FIELD_NAMES, ALLOW_SINGLE_QUOTES 등 Jackson의 유연한 파싱 기능과 보안상의 주의점을 함께 설명합니다.

728x90