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;
}
}