ObjectMapper 개념 및 사용

핵심 요약

  • ObjectMapper는 자바 객체(Java Object)와 JSON 사이의 양방향 통역사 역할을 하는 Jackson 라이브러리의 핵심 클래스입니다.
  • 직렬화(Serialization)는 객체를 JSON 문자열로 변환하는 것이며, 역직렬화(Deserialization)는 JSON 문자열을 객체로 변환하는 것입니다.
  • JSON 배열을 자바의 List로 변환할 때는 반드시 TypeReference를 사용하여 제네릭 타입 소거(Type Erasure) 문제를 방지해야 합니다.
  • 실무에서는 FAIL_ON_UNKNOWN_PROPERTIES 옵션을 끄고, 날짜 처리를 위해 JavaTimeModule을 등록하여 Bean으로 관리하는 것이 표준입니다.

목차

안녕하세요. Spring Boot 웹 개발의 필수품, Jackson ObjectMapper 가이드입니다. 많은 개발자가 ObjectMapper를 사용하지만, 정작 내부 동작이나 안전한 사용법을 놓치는 경우가 많습니다. 오늘은 자바 객체와 JSON 사이를 오가는 가장 확실한 방법을 예제 코드와 함께 정리해보겠습니다.

# 사전 준비: 예제용 DTO 클래스

본 포스팅의 모든 예제 코드에서 사용할 데이터 모델입니다. Java 17 이상에서 지원하는 record 타입을 사용하여 간결하게 정의했습니다.

User.java

// Java 17+ Record (DTO로 가정)
public record User(String name, int age, String email) {}

1. ObjectMapper의 핵심 역할 3가지

ObjectMapper는 단순히 데이터를 바꾸는 도구가 아닙니다. 서버와 클라이언트 간의 대화를 가능하게 하는 통역사입니다.

1. Serialization (직렬화)

자바 객체(Entity, DTO)를 JSON 문자열로 변환합니다. 주로 서버에서 클라이언트로 응답(Response)을 보낼 때 사용됩니다.

2. Deserialization (역직렬화)

JSON 문자열을 자바 객체로 변환합니다. 클라이언트가 보낸 요청 바디(Request Body)를 처리할 때 사용됩니다.

3. Transformation (변환)

JSON을 파싱하여 특정 필드만 읽거나, 구조를 재조립합니다. 외부 API 연동 시 유용합니다.

2. 실무 필수 메서드 및 사용 예시

가장 빈번하게 사용되는 5가지 핵심 패턴을 코드로 살펴보겠습니다.

① 객체를 JSON으로 변환 (writeValueAsString)

API 응답 로그를 남기거나 외부로 데이터를 전송할 때 사용하는 가장 기본적인 직렬화 메서드입니다.

import com.fasterxml.jackson.databind.ObjectMapper;

public class SerializationExample {
  public void convertToJson() {
    ObjectMapper objectMapper = new ObjectMapper();
    User user = new User("SkyStat_Dev", 32, "dev@ilways.com");

    try {
      // Java Object -> JSON String
      String jsonResult = objectMapper.writeValueAsString(user);
      System.out.println(jsonResult); 
      // 출력: {"name":"SkyStat_Dev","age":32,"email":"dev@ilways.com"}

    } catch (Exception e) {
      // 예외 메시지는 영어로, 주석은 한글로 설명
      throw new RuntimeException("Failed to serialize object to JSON", e); // 객체를 JSON으로 직렬화하는데 실패했습니다.
    }
  }
}

② JSON을 객체로 변환 (readValue)

외부에서 들어온 JSON 데이터를 자바 객체로 매핑합니다.

public class DeserializationExample {
  public void convertToObject() {
    ObjectMapper objectMapper = new ObjectMapper();
    String json = "{\"name\":\"SkyStat_Dev\",\"age\":32,\"email\":\"dev@ilways.com\"}";

    try {
      // JSON String -> Java Object
      User user = objectMapper.readValue(json, User.class);
      System.out.println("User Name: " + user.name());

    } catch (Exception e) {
      throw new RuntimeException("Failed to deserialize JSON to object", e); // JSON을 객체로 역직렬화하는데 실패했습니다.
    }
  }
}

③ JSON 배열을 리스트로 변환 (TypeReference) [중요]

실무에서 가장 많이 실수하는 부분입니다. List<User> 같은 제네릭 타입으로 변환할 때는 반드시 TypeReference를 사용해야 안전합니다.

import com.fasterxml.jackson.core.type.TypeReference;
import java.util.List;

public class ListDeserializationExample {
  public void convertToList() {
    ObjectMapper objectMapper = new ObjectMapper();
    String jsonArray = "[{\"name\":\"User1\",\"age\":20}, {\"name\":\"User2\",\"age\":30}]";

    try {
      // JSON Array -> List
      // TypeReference를 사용하여 제네릭 타입을 명시해야 함
      List userList = objectMapper.readValue(jsonArray, new TypeReference>() {});
      
      userList.forEach(u -> System.out.println(u.name()));

    } catch (Exception e) {
      throw new RuntimeException("Failed to parse JSON array", e); // JSON 배열 파싱에 실패했습니다.
    }
  }
}

④ JSON 구조가 불확실할 때 (readTree, JsonNode)

DTO를 만들기 애매하거나 특정 필드 값만 추출하고 싶을 때 사용합니다. path() 메서드는 Null 안전성을 보장하므로 get()보다 권장됩니다.

import com.fasterxml.jackson.databind.JsonNode;

public class TreeModelExample {
  public void readSpecificField() {
    ObjectMapper objectMapper = new ObjectMapper();
    String json = "{\"response\": {\"status\": \"OK\", \"data\": {\"id\": 100}}}";

    try {
      JsonNode rootNode = objectMapper.readTree(json);
      
      // 중첩된 구조 접근 (response -> data -> id)
      // path()는 null 안전성을 보장하므로 실무에서 get()보다 권장됨
      JsonNode idNode = rootNode.path("response").path("data").path("id");
      
      int id = idNode.asInt(0); // 값이 없거나 변환 실패 시 기본값 0
      System.out.println("ID: " + id);

    } catch (Exception e) {
      throw new RuntimeException("Error processing JSON tree", e); // JSON 트리 처리 중 오류가 발생했습니다.
    }
  }
}

⑤ 객체 간 변환 (convertValue)

Map을 DTO로 변환하거나, DTO를 다른 DTO로 변환할 때 매우 유용합니다. 내부적으로 직렬화/역직렬화를 수행하여 값을 복사합니다.

import java.util.Map;

public class ConvertExample {
  public void mapToPojo() {
    ObjectMapper objectMapper = new ObjectMapper();
    Map map = Map.of("name", "SkyStat", "age", 32);

    // Map -> User Object (별도의 파싱 과정 없이 변환)
    // 캐스팅(casting)이 아니라, 내부적으로 직렬화->역직렬화 과정을 거쳐 값을 복사함
    User user = objectMapper.convertValue(map, User.class);
  }
}

3. 실무 필수 설정 (Spring Boot 환경)

Spring Boot에서는 ObjectMapper를 직접 new로 생성하기보다, 설정(Configuration) 클래스에서 Bean으로 등록하여 주입받아 사용하는 것이 정석입니다.

JacksonConfig.java

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JacksonConfig {

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

    // [중요] 모르는 속성 무시 (FAIL_ON_UNKNOWN_PROPERTIES)
    // 설명: JSON에는 있는데 DTO에는 없는 필드가 있어도 에러를 내지 않고 무시합니다.
    // API 버전 변경 시 유연성을 위해 거의 필수 옵션입니다.
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    // [중요] 날짜/시간 지원 (JavaTimeModule)
    // 설명: LocalDateTime, ZonedDateTime 등을 올바르게 처리하기 위해 등록합니다.
    objectMapper.registerModule(new JavaTimeModule());

    return objectMapper;
  }
}

4. 요약: 언제 무엇을 써야 할까?

마지막으로 상황별 적절한 메서드를 표로 정리해 드립니다.

상황 메서드 비고
객체 → JSON writeValueAsString(obj) 로그 저장, API 요청 바디 생성 시 사용
JSON → 객체 readValue(json, Class) 단일 객체를 파싱할 때 사용
JSON → 리스트 readValue(json, TypeReference) List<T> 등 제네릭 타입 파싱 시 필수
특정 필드만 조회 readTree(json) + .path() DTO 만들기 귀찮거나 구조가 복잡할 때 유용
Map ↔ 객체 convertValue(from, to) 데이터 타입 변환 시 유용 (Reflection 대체)

이 내용들을 바탕으로 현재 개발 중인 코드에서 ObjectMapper를 더 적절하고 안전하게 활용해 보시길 바랍니다.

이 글은 어떠셨나요? 자유롭게 의견을 남겨주세요! 💬