Jackson 라이브러리를 활용한 JSON 직렬화/역직렬화 어노테이션 심층 가이드

Java 애플리케이션에서 JSON 데이터를 효율적으로 처리하기 위해 Jackson 라이브러리는 광범위하게 사용됩니다. Jackson은 객체를 JSON 문자열로 변환(직렬화)하거나 JSON 문자열을 객체로 변환(역직렬화)하는 과정을 매우 유연하게 제어할 수 있는 다양한 어노테이션을 제공합니다. 이 가이드에서는 주요 Jackson 어노테이션의 기능과 활용 방법을 자세히 살펴봅니다.

@JsonNaming

@JsonNaming 어노테이션은 클래스 수준에서 필드 이름의 직렬화/역직렬화 규칙을 정의할 때 사용됩니다. 예를 들어, 자바의 카멜케이스(camelCase) 필드 이름을 JSON의 스네이크케이스(snake_case)로 자동 변환하고 싶을 때 유용합니다. 이를 통해 코드베이스를 변경하지 않고도 JSON 포맷 규칙을 따를 수 있습니다.

@JsonIgnoreProperties

객체를 JSON으로 변환할 때 특정 필드들을 무시하고 싶을 수 있습니다. @JsonIgnoreProperties는 클래스 수준에서 하나 이상의 필드를 직렬화/역직렬화 대상에서 제외시킵니다. 또한, ignoreUnknown = true 옵션을 사용하면 역직렬화 시 JSON에 존재하지만 자바 객체에 없는 필드를 무시하여 예외 발생을 방지할 수 있습니다.

@JsonIgnore

@JsonIgnore 어노테이션은 특정 필드 하나를 직렬화/역직렬화 대상에서 제외할 때 사용됩니다. 주로 민감한 정보(예: 비밀번호)를 JSON에 포함하지 않기 위해 필드 수준에서 적용합니다.

@JsonFormat

날짜와 시간 값은 다양한 형식으로 표현될 수 있습니다. @JsonFormat은 날짜/시간 필드를 특정 패턴(예: "yyyy-MM-dd HH:mm:ss")으로 직렬화하거나 역직렬화할 수 있도록 지정합니다. 이는 기본 날짜 형식을 오버라이드할 때 유용합니다.

@JsonDeserialize

기본 Jackson 역직렬화 로직이 충분하지 않을 때, 사용자 정의 역직렬화 로직을 구현할 수 있습니다. @JsonDeserialize 어노테이션은 특정 필드에 대해 사용자 정의 JsonDeserializer 클래스를 지정하여, JSON 값을 객체 필드 타입으로 변환하는 방식을 세밀하게 제어할 수 있도록 합니다.

@JsonSerialize

마찬가지로, 객체 필드를 JSON 값으로 직렬화하는 기본 로직을 변경하고 싶을 때 @JsonSerialize 어노테이션을 사용합니다. 이 어노테이션은 특정 필드에 대해 사용자 정의 JsonSerializer 클래스를 지정하여, 객체 필드 값을 JSON 문자열로 변환하는 방식을 맞춤 설정할 수 있습니다.

@JsonProperty

@JsonProperty 어노테이션은 여러 용도로 사용됩니다. 주로 자바 필드 이름과 JSON 필드 이름이 다를 경우 매핑을 정의하거나, getter/setter 메서드가 없는 필드를 직렬화/역직렬화 대상에 포함시킬 때 사용합니다.

아래 UserProfile 클래스 예시를 통해 위 어노테이션들의 실제 적용 방법을 확인해 봅시다.

package dev.example.json.model;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import dev.example.json.util.KoreanDateDeserializer;
import dev.example.json.util.FormattedDoubleSerializer;

import java.math.BigDecimal;
import java.util.Date;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@JsonIgnoreProperties(value = {"active", "score", "depositAmount"}, ignoreUnknown = true)
public class UserProfile {

    private String accountName;

    @JsonIgnore
    private String secureKey;

    @JsonFormat(pattern = "yyyy-MM-dd")
    @JsonDeserialize(using = KoreanDateDeserializer.class)
    private Date registrationDate;

    private boolean active;
    private int score;
    private BigDecimal depositAmount;

    @JsonSerialize(using = FormattedDoubleSerializer.class)
    private double currentBalance;

    // 해당 필드는 기본 getter/setter가 없지만 JSON에 포함시키기 위해 @JsonProperty 사용
    @JsonProperty("role_assignment_status")
    private boolean assignedRole = false;

    public UserProfile() {
        // 기본 생성자
    }

    // Constructor for convenience
    public UserProfile(String accountName, String secureKey, Date registrationDate, boolean active, int score, BigDecimal depositAmount, double currentBalance) {
        this.accountName = accountName;
        this.secureKey = secureKey;
        this.registrationDate = registrationDate;
        this.active = active;
        this.score = score;
        this.depositAmount = depositAmount;
        this.currentBalance = currentBalance;
    }

    // Getters and Setters
    public String getAccountName() {
        return accountName;
    }

    public void setAccountName(String accountName) {
        this.accountName = accountName;
    }

    public String getSecureKey() {
        return secureKey;
    }

    public void setSecureKey(String secureKey) {
        this.secureKey = secureKey;
    }

    public Date getRegistrationDate() {
        return registrationDate;
    }

    public void setRegistrationDate(Date registrationDate) {
        this.registrationDate = registrationDate;
    }

    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }

    public BigDecimal getDepositAmount() {
        return depositAmount;
    }

    public void setDepositAmount(BigDecimal depositAmount) {
        this.depositAmount = depositAmount;
    }

    public double getCurrentBalance() {
        return currentBalance;
    }

    public void setCurrentBalance(double currentBalance) {
        this.currentBalance = currentBalance;
    }

    // No setter for assignedRole to demonstrate @JsonProperty without explicit getter/setter
    public boolean isAssignedRole() {
        return assignedRole;
    }

    @Override
    public String toString() {
        return "UserProfile{" +
               "accountName='" + accountName + '\'' +
               ", secureKey='" + secureKey + '\'' +
               ", registrationDate=" + registrationDate +
               ", active=" + active +
               ", score=" + score +
               ", depositAmount=" + depositAmount +
               ", currentBalance=" + currentBalance +
               ", assignedRole=" + assignedRole +
               '}';
    }
}

UserProfile 클래스에서 사용된 사용자 정의 직렬화/역직렬화 클래스들은 다음과 같습니다.

package dev.example.json.util;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class KoreanDateDeserializer extends JsonDeserializer<Date> {

    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

    @Override
    public Date deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
        String dateString = jsonParser.getText();
        try {
            return DATE_FORMAT.parse(dateString);
        } catch (ParseException e) {
            // 역직렬화 실패 시 오류 처리 (예: null 반환, 예외 발생 등)
            throw new IOException("Failed to deserialize date: " + dateString, e);
        }
    }
}
package dev.example.json.util;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;
import java.text.DecimalFormat;

public class FormattedDoubleSerializer extends JsonSerializer<Double> {

    private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.00"); // 항상 소수점 두 자리

    @Override
    public void serialize(Double value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException {
        if (value == null) {
            jsonGenerator.writeNull();
        } else {
            jsonGenerator.writeString(DECIMAL_FORMAT.format(value));
        }
    }
}

Jackson 어노테이션의 기능을 테스트하기 위한 예제 코드입니다.

package dev.example.json.test;

import com.fasterxml.jackson.databind.ObjectMapper;
import dev.example.json.model.UserProfile;
import dev.example.json.util.JsonProcessorUtil;
import org.junit.jupiter.api.Test; // JUnit 5 사용
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;
import java.util.Date;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class JacksonConversionTests {
    private static final JsonProcessorUtil jsonProcessor = new JsonProcessorUtil();
    private static final Logger logger = LoggerFactory.getLogger(JacksonConversionTests.class);

    /**
     * UserProfile 객체를 JSON 문자열로 직렬화 테스트
     */
    @Test
    void testUserProfileSerialization() {
        UserProfile user = new UserProfile(
                "tester_account",
                "secret_password_123",
                new Date(),
                true,
                95,
                new BigDecimal("1000.50"),
                45.1234
        );

        // assignedRole 필드 값 설정 (JsonProperty 테스트 목적)
        // user.setAssignedRole(true); // 직접적인 setter가 없으므로 Reflection 등을 사용해야 하나,
                                    // 여기서는 초기값 false로 테스트하거나 생성자에서 처리 가정

        String jsonOutput = jsonProcessor.serializeObject(user);
        logger.info("Serialized JSON: {}", jsonOutput);

        assertNotNull(jsonOutput);
        // 예상되는 직렬화 결과 확인
        // - secureKey는 @JsonIgnore로 인해 제외
        // - active, score, depositAmount는 @JsonIgnoreProperties로 인해 제외
        // - accountName은 snake_case로 변환 (account_name)
        // - registrationDate는 yyyy-MM-dd 형식 (registration_date)
        // - currentBalance는 0.00 형식 (current_balance)
        // - assignedRole은 role_assignment_status로 매핑
    }

    /**
     * JSON 문자열을 UserProfile 객체로 역직렬화 테스트
     */
    @Test
    void testUserProfileDeserialization() {
        // @JsonFormat, @JsonDeserialize, @JsonIgnoreProperties(ignoreUnknown=true) 테스트
        String inputJson = "{" +
                           "\"account_name\":\"guest_user\"," +
                           "\"secureKey\":\"hidden\"," + // @JsonIgnore로 인해 무시될 값
                           "\"registration_date\":\"2023-04-15\"," +
                           "\"active\":false," +         // @JsonIgnoreProperties로 인해 무시될 값
                           "\"unknown_field\":\"some_value\"," + // ignoreUnknown=true로 인해 무시될 값
                           "\"score\":70," +              // @JsonIgnoreProperties로 인해 무시될 값
                           "\"deposit_amount\":500.25," + // @JsonIgnoreProperties로 인해 무시될 값
                           "\"current_balance\":\"78.99\"," +
                           "\"role_assignment_status\":true" +
                           "}";

        UserProfile deserializedUser = jsonProcessor.deserializeObject(inputJson, UserProfile.class);
        logger.info("Deserialized UserProfile: {}", deserializedUser);

        assertNotNull(deserializedUser);
        assertEquals("guest_user", deserializedUser.getAccountName());
        assertEquals(null, deserializedUser.getSecureKey()); // @JsonIgnore로 인해 null
        assertNotNull(deserializedUser.getRegistrationDate()); // @JsonFormat 및 CustomDeserializer 적용
        assertEquals(0, deserializedUser.getScore()); // @JsonIgnoreProperties로 인해 기본값
        assertEquals(null, deserializedUser.getDepositAmount()); // @JsonIgnoreProperties로 인해 기본값
        assertEquals(78.99, deserializedUser.getCurrentBalance(), 0.001); // CustomSerializer 적용
        assertEquals(true, deserializedUser.isAssignedRole()); // @JsonProperty 적용
    }
}

마지막으로, Jackson의 기능을 캡슐화하고 재사용성을 높인 JSON 처리 유틸리티 클래스 JsonProcessorUtil입니다. 이 유틸리티는 다양한 직렬화 포함 전략을 지원합니다.

package dev.example.json.util;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.JSONPObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils; // Spring Framework의 StringUtils 사용, 또는 Apache Commons Lang3

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Collection;

/**
 * Jackson ObjectMapper를 활용한 JSON 직렬화/역직렬화 유틸리티.
 * Springside 프로젝트에서 영감을 받아 재구성되었습니다.
 */
public class JsonProcessorUtil {

    private static final Logger logger = LoggerFactory.getLogger(JsonProcessorUtil.class);
    private final ObjectMapper objectMapper;

    /**
     * 기본 설정의 JsonProcessorUtil 생성자.
     * 날짜 포맷은 "yyyy-MM-dd HH:mm:ss"를 사용하며, 알려지지 않은 JSON 속성을 무시합니다.
     */
    public JsonProcessorUtil() {
        this(null);
    }

    /**
     * 특정 JsonInclude.Include 전략을 사용하여 JsonProcessorUtil을 생성합니다.
     *
     * @param include 직렬화에 포함할 속성의 기준 (예: NON_EMPTY, NON_NULL)
     */
    public JsonProcessorUtil(JsonInclude.Include include) {
        this.objectMapper = new ObjectMapper();
        // 날짜/시간 직렬화/역직렬화 기본 포맷 설정
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        this.objectMapper.setDateFormat(dateFormat);

        // 직렬화 시 속성 포함 스타일 설정
        if (include != null) {
            this.objectMapper.setSerializationInclusion(include);
        }

        // 역직렬화 시 JSON에 존재하지만 Java 객체에 없는 속성 무시
        this.objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    }

    /**
     * 필드가 null이거나 비어있는("") 경우 직렬화에서 제외하는 유틸리티 인스턴스를 반환합니다.
     * @return JsonProcessorUtil 인스턴스
     */
    public static JsonProcessorUtil createNonEmptyProcessor() {
        return new JsonProcessorUtil(JsonInclude.Include.NON_EMPTY);
    }

    /**
     * 필드가 기본값인 경우 직렬화에서 제외하는 유틸리티 인스턴스를 반환합니다.
     * @return JsonProcessorUtil 인스턴스
     */
    public static JsonProcessorUtil createNonDefaultProcessor() {
        return new JsonProcessorUtil(JsonInclude.Include.NON_DEFAULT);
    }

    /**
     * 필드가 null인 경우 직렬화에서 제외하는 유틸리티 인스턴스를 반환합니다.
     * @return JsonProcessorUtil 인스턴스
     */
    public static JsonProcessorUtil createNonNullProcessor() {
        return new JsonProcessorUtil(JsonInclude.Include.NON_NULL);
    }

    /**
     * 객체를 JSON 문자열로 직렬화합니다.
     * 객체가 POJO, Collection, 또는 배열일 수 있습니다.
     * 객체가 null이면 "null" 문자열을 반환하고, 빈 컬렉션이면 "[]"를 반환합니다.
     *
     * @param object 직렬화할 객체
     * @return JSON 문자열 또는 직렬화 실패 시 null
     */
    public String serializeObject(Object object) {
        try {
            return this.objectMapper.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            logger.warn("객체({})를 JSON 문자열로 직렬화하는 데 실패했습니다.", object, e);
            return null;
        }
    }

    /**
     * JSON 문자열을 POJO 또는 간단한 컬렉션(예: List<String>)으로 역직렬화합니다.
     * JSON 문자열이 null이거나 "null"이면 null을 반환합니다. "[]"이면 빈 컬렉션을 반환합니다.
     * 복잡한 컬렉션(예: List<MyBean>) 역직렬화에는 {@link #deserializeObject(String, JavaType)}를 사용하십시오.
     *
     * @param jsonString 역직렬화할 JSON 문자열
     * @param targetClass 역직렬화될 대상 클래스
     * @return 역직렬화된 객체 또는 실패 시 null
     */
    public <T> T deserializeObject(String jsonString, Class<T> targetClass) {
        if (!StringUtils.hasText(jsonString)) { // Spring StringUtils.hasText()
            return null;
        }
        try {
            return this.objectMapper.readValue(jsonString, targetClass);
        } catch (IOException e) {
            logger.warn("JSON 문자열({})을 객체로 역직렬화하는 데 실패했습니다.", jsonString, e);
            return null;
        }
    }

    /**
     * JSON 문자열을 복잡한 컬렉션(예: List<MyBean>)으로 역직렬화합니다.
     * 먼저 {@link #constructCollectionType(Class, Class...)}를 사용하여 JavaType을 생성한 후 이 메서드를 호출합니다.
     *
     * @param jsonString 역직렬화할 JSON 문자열
     * @param javaType 역직렬화될 대상 JavaType
     * @return 역직렬화된 객체 또는 실패 시 null
     */
    @SuppressWarnings("unchecked")
    public <T> T deserializeObject(String jsonString, JavaType javaType) {
        if (!StringUtils.hasText(jsonString)) {
            return null;
        }
        try {
            return (T) this.objectMapper.readValue(jsonString, javaType);
        } catch (IOException e) {
            logger.warn("JSON 문자열({})을 JavaType({})으로 역직렬화하는 데 실패했습니다.", jsonString, javaType, e);
            return null;
        }
    }

    /**
     * 제네릭 컬렉션 타입을 생성합니다.
     * 예: ArrayList<MyBean>을 위해서는 constructCollectionType(ArrayList.class, MyBean.class) 호출.
     * HashMap<String, MyBean>을 위해서는 constructCollectionType(HashMap.class, String.class, MyBean.class) 호출.
     *
     * @param collectionClass 컬렉션 클래스 (예: List.class, Map.class)
     * @param elementClasses 컬렉션 요소의 클래스 (예: MyBean.class, String.class)
     * @return 구성된 JavaType
     */
    public JavaType constructCollectionType(Class<? extends Collection> collectionClass, Class<?>... elementClasses) {
        return this.objectMapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
    }

    /**
     * 기존 객체를 JSON 문자열로 업데이트합니다.
     * @param jsonString 업데이트에 사용할 JSON 문자열
     * @param object 업데이트될 기존 객체
     * @return 업데이트된 객체 또는 실패 시 null
     */
    public <T> T updateObject(String jsonString, T object) {
        try {
            return this.objectMapper.readerForUpdating(object).readValue(jsonString);
        } catch (JsonProcessingException e) {
            logger.warn("객체({})를 JSON 문자열({})로 업데이트하는 데 실패했습니다.", object, jsonString, e);
        } catch (IOException e) {
            logger.warn("객체({})를 JSON 문자열({})로 업데이트하는 데 실패했습니다.", object, jsonString, e);
        }
        return null;
    }

    /**
     * JSONP 형식으로 객체를 직렬화합니다.
     * @param functionName JSONP 콜백 함수 이름
     * @param object 직렬화할 객체
     * @return JSONP 형식의 문자열
     */
    public String serializeToJsonP(String functionName, Object object) {
        return this.serializeObject(new JSONPObject(functionName, object));
    }

    /**
     * 내부 ObjectMapper 인스턴스를 반환합니다.
     * @return ObjectMapper 인스턴스
     */
    public ObjectMapper getObjectMapper() {
        return objectMapper;
    }
}

태그: Jackson JSON java serialization deserialization

6월 30일 16:12에 게시됨