콘텐츠 대표 이미지 - Jackson 마스터 가이드: JSON, 가지고 놀 준비 됐어? 🚀

Jackson 마스터 가이드: JSON, 가지고 놀 준비 됐어? 🚀

Java 개발자라면 피할 수 없는 JSON!
이 복잡한 데이터를 내 손안에서 주무르는 비법, Jackson의 모든 것을 알려줄게!

들어가며: JSON과 Jackson, 왜 이 조합이 '국룰'인가? 🤔

안녕, 코딩 메이트! 오늘도 수많은 데이터와 씨름하며 멋진 서비스를 만들어가고 있겠지?
특히 요즘 개발 씬에서 '데이터'하면 빼놓을 수 없는 게 바로 JSON(JavaScript Object Notation)이야.
클라이언트와 서버가 서로 대화하는 언어, 설정 파일의 표준, NoSQL 데이터베이스의 기본 포맷까지...
어딜 가나 JSON, JSON, JSON! 그야말로 데이터계의 '인싸'라고 할 수 있지. 😎

그런데 우리 Java 개발자들에게 이 JSON은 가끔은 애증의 대상이 되기도 해.
문자열로 된 데이터를 일일이 파싱해서 객체에 담고, 다시 객체를 문자열로 바꾸는 그 귀찮은 과정...
생각만 해도 머리가 지끈거리지 않아?

바로 이때, 구원투수처럼 등판하는 라이브러리가 있었으니... 그 이름은 바로 Jackson! 🌟
Jackson은 Java 세계에서 JSON 데이터를 다루는 작업을 '혁명적으로' 바꿔놓은 라이브러리야.
마치 복잡한 요리를 뚝딱 해치우는 만능 주방 도구처럼, JSON 직렬화(Serialization)와 역직렬화(Deserialization)를 너무나도 우아하고 간편하게 처리해주지.

잠깐, 용어 정리 타임! 💡

직렬화(Serialization): Java 객체(Object)를 JSON 같은 데이터 포맷(주로 문자열)으로 변환하는 과정을 말해.
네트워크로 전송하거나 파일에 저장하기 위해 데이터를 '일렬로' 늘어놓는다고 생각하면 쉬워.

역직렬화(Deserialization): 직렬화의 반대 과정! JSON 같은 데이터 포맷을 다시 Java 객체로 되살리는 과정이야.
문자열 데이터를 우리가 코드에서 다룰 수 있는 살아있는 객체로 만드는 거지.

물론 Jackson 말고도 Gson, JSON-B 같은 좋은 라이브러리들이 있어.
하지만 Jackson이 유독 '사실상의 표준(De facto standard)'으로 불리는 데는 다 이유가 있겠지?

첫째, 압도적인 성능. Jackson은 스트리밍 API를 기반으로 설계되어 대용량 JSON 데이터도 메모리 효율적으로 빠르게 처리할 수 있어.
둘째, 막강한 유연성과 확장성. 간단한 POJO(Plain Old Java Object) 매핑부터 복잡한 커스텀 로직까지, 어노테이션과 모듈 시스템을 통해 거의 모든 요구사항을 만족시킬 수 있어.
셋째, 거대한 생태계. 특히 Spring 프레임워크와의 환상적인 궁합은 Jackson을 Java 웹 개발의 필수 라이브러리로 만들었지. Spring Boot를 쓴다면 이미 당신도 모르는 사이에 Jackson의 도움을 받고 있을 확률이 99%야!

이 글에서는 Jackson의 기초부터 현업에서 마주칠 수 있는 고급 테크닉과 트러블슈팅까지, A to Z를 모두 다뤄볼 거야.
마치 친한 친구가 옆에서 알려주듯, 쉽고 재미있게 Jackson의 세계로 안내할게.
자, 이제 커피 한 잔 들고 스크롤 내릴 준비 됐어? Let's go! ☕️

1단계: Jackson 날개 달아주기 (프로젝트 설정) 🛠️

모든 여정의 시작은 준비운동부터! 우리 프로젝트에 Jackson이라는 강력한 엔진을 장착해보자.
Java 프로젝트의 의존성 관리는 주로 Maven이나 Gradle을 사용하니까, 각각의 경우를 모두 보여줄게.

Jackson은 여러 개의 모듈로 구성되어 있는데, 가장 핵심적인 3총사가 있어.

1. jackson-core: 이름 그대로 Jackson의 핵심 기능이야. 저수준의 스트리밍 API(JsonParser, JsonGenerator)를 포함하고 있어. 모든 Jackson 모듈의 기반이 되지.
2. jackson-annotations: `@JsonProperty`, `@JsonIgnore`처럼 JSON 처리 방식을 제어하는 다양한 어노테이션을 제공해. 디테일한 컨트롤의 시작이지.
3. jackson-databind: 이 녀석이 바로 우리가 가장 많이 사용하게 될 주인공! `jackson-core`와 `jackson-annotations`에 의존하면서, Java 객체와 JSON 간의 데이터 바인딩(매핑)을 자동으로 처리해주는 고수준 API(`ObjectMapper`)를 제공해.

결론적으로, 우리는 `jackson-databind`만 의존성에 추가하면 돼. 그러면 Maven이나 Gradle이 알아서 `core`와 `annotations`를 함께 가져와 주거든. 똑똑한 녀석들!

Maven 설정 (`pom.xml`)

`pom.xml` 파일의 `` 섹션에 아래 코드를 추가해줘.


<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.2</version> <!-- 최신 안정 버전을 사용하는 것을 추천! -->
</dependency>
    

Gradle 설정 (`build.gradle`)

`build.gradle` 파일의 `dependencies` 블록에 아래 코드를 추가하면 끝!


implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' // 역시 최신 버전 확인은 필수!
    
프로 팁: 버전 관리 🧑‍💻

라이브러리 버전은 계속 업데이트돼. 보안 취약점이 해결되거나 새로운 기능이 추가되기도 하니,
Maven Central Repository 같은 곳에서 주기적으로 최신 안정 버전을 확인하고 업데이트하는 습관을 들이는 게 좋아.
너무 오래된 버전을 사용하면 나중에 호환성 문제나 보안 이슈로 고생할 수 있거든.

자, 이제 우리 프로젝트는 Jackson을 사용할 모든 준비를 마쳤어.
의존성 추가라는 작은 날갯짓이 앞으로 얼마나 엄청난 코딩 효율을 가져다줄지, 곧 체감하게 될 거야.
다음 챕터에서는 Jackson의 심장이자 두뇌인 `ObjectMapper`를 만나러 가보자!

Jackson의 핵심: ObjectMapper의 역할 Java Object (POJO) class User { String name; int age; } ObjectMapper 마법의 변환기 JSON String { "name": "재능이", "age": 25 } 직렬화 (Serialization) writeValueAsString() 역직렬화 (Deserialization) readValue()

2단계: ObjectMapper와 첫 만남 (기본 사용법) 🤝

자, 드디어 Jackson의 핵심 플레이어, ObjectMapper를 만나볼 시간이야.
이름부터 '오브젝트를 매핑하는 녀석'이라는 정체성이 확실하지? 이 친구 하나만 있으면 JSON 변환은 거의 끝났다고 봐도 무방해.

`ObjectMapper`는 Java 객체를 JSON으로, JSON을 Java 객체로 변환하는 모든 작업을 총괄하는 지휘자야.
사용법은 놀랄 만큼 간단해. 딱 두 가지 메서드만 기억하면 기본기는 마스터할 수 있어.

1. `writeValueAsString(Object obj)`: Java 객체를 JSON 형태의 문자열로 변환 (직렬화)
2. `readValue(String json, Class<T> clazz)`: JSON 형태의 문자열을 지정된 Java 클래스 객체로 변환 (역직렬화)

백문이 불여일견! 바로 코드로 확인해보자.
먼저, 우리가 변환할 간단한 `User` 클래스를 하나 만들게.


// User.java
public class User {
    private String name;
    private int age;
    private String email;

    // Jackson은 역직렬화 시 기본 생성자를 사용해. 꼭 만들어주자!
    public User() {
    }

    public User(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }

    // Jackson은 필드에 직접 접근하거나, getter/setter를 통해 값을 읽고 쓸 수 있어.
    // 일반적으로는 private 필드 + public getter/setter 패턴을 사용해.
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", email='" + email + '\'' +
                '}';
    }
}
    
⚠️ 잠깐! 중요한 포인트!

Jackson이 JSON을 Java 객체로 만들 때(역직렬화), 내부적으로 기본 생성자(매개변수 없는 생성자)를 호출해서 객체를 먼저 생성한 뒤, setter 메서드나 필드 리플렉션을 통해 값을 채워 넣어.
따라서, POJO 클래스에는 반드시 기본 생성자가 있어야 해! 만약 다른 생성자를 만들었다면, 컴파일러가 자동으로 만들어주지 않으니 꼭 명시적으로 추가해줘야 한다는 점, 잊지 마!

1. 직렬화: Java 객체 ➡️ JSON 문자열

`User` 객체를 하나 만들어서 JSON 문자열로 바꿔보자.


import com.fasterxml.jackson.databind.ObjectMapper;

public class JacksonBasicExample {
    public static void main(String[] args) throws Exception { // 예외 처리는 간단하게!
        // 1. ObjectMapper 인스턴스 생성
        ObjectMapper objectMapper = new ObjectMapper();

        // 2. JSON으로 변환할 Java 객체 생성
        User user = new User("김재능", 30, "jaenung@example.com");

        // 3. writeValueAsString() 메서드로 직렬화 수행!
        String jsonString = objectMapper.writeValueAsString(user);

        // 4. 결과 확인
        System.out.println(jsonString);
        // 출력 결과: {"name":"김재능","age":30,"email":"jaenung@example.com"}
    }
}
    

어때? 너무 간단하지 않아? 🤯
`ObjectMapper`를 만들고, 객체를 `writeValueAsString` 메서드에 던져주기만 하면 끝!
길고 복잡한 문자열 조합 코드 없이, 단 한 줄로 완벽한 JSON이 만들어졌어.

2. 역직렬화: JSON 문자열 ➡️ Java 객체

이번엔 반대로, JSON 문자열을 다시 `User` 객체로 되살려보자.


import com.fasterxml.jackson.databind.ObjectMapper;

public class JacksonBasicExample2 {
    public static void main(String[] args) throws Exception {
        // 1. ObjectMapper 인스턴스 생성
        ObjectMapper objectMapper = new ObjectMapper();

        // 2. Java 객체로 변환할 JSON 문자열 준비
        String jsonString = "{\"name\":\"박코딩\",\"age\":28,\"email\":\"coding@example.com\"}";

        // 3. readValue() 메서드로 역직렬화 수행!
        // 첫 번째 인자: JSON 문자열, 두 번째 인자: 변환할 클래스 타입
        User user = objectMapper.readValue(jsonString, User.class);

        // 4. 결과 확인
        System.out.println(user);
        // 출력 결과: User{name='박코딩', age=28, email='coding@example.com'}
        System.out.println("이름: " + user.getName()); // 이름: 박코딩
    }
}
    

이것도 마찬가지로 `readValue` 메서드 한 줄이면 게임 끝!
Jackson이 JSON의 키(`"name"`, `"age"`)를 보고 알아서 `User` 클래스의 해당 필드나 setter(`setName`, `setAge`)를 찾아 값을 채워주는 거야. 정말 똑똑하지?

이것만으로도 이미 Jackson의 강력함을 충분히 느꼈을 거야.
하지만 Jackson의 진정한 매력은 지금부터 시작이야.
다음 챕터에서는 `ObjectMapper`를 우리 입맛에 맞게 설정해서 더 멋지게 활용하는 방법을 알아볼게!

3단계: ObjectMapper 커스터마이징 (feat. Configuration) 🎨

기본적인 변환은 이제 식은 죽 먹기! 하지만 실무에서는 생각보다 다양한 요구사항들이 존재해.
"JSON 출력을 좀 더 보기 좋게 할 순 없을까?", "JSON에 있는 필드가 내 객체에는 없는데, 에러 없이 무시하고 싶어!" 같은 상황들 말이야.

걱정 마! `ObjectMapper`는 이런 디테일한 설정들을 아주 쉽게 제어할 수 있는 옵션들을 제공해.
`configure()` 메서드와 `SerializationFeature`, `DeserializationFeature` 열거형(Enum)을 사용하면 돼.

보기 좋은 JSON이 읽기도 좋다: `SerializationFeature.INDENT_OUTPUT`

앞에서 직렬화된 JSON 결과는 `{"name":"김재능","age":30,"email":"jaenung@example.com"}` 처럼 한 줄로 쭉 이어져 있었지?
기계가 읽기엔 효율적이지만, 사람이 디버깅하거나 로그를 확인할 땐 가독성이 떨어져.
이때 `INDENT_OUTPUT` 옵션을 켜주면, 예쁘게 들여쓰기 된 JSON(Pretty Print)을 얻을 수 있어.


ObjectMapper objectMapper = new ObjectMapper();
// INDENT_OUTPUT 기능을 활성화!
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);

User user = new User("김재능", 30, "jaenung@example.com");
String prettyJson = objectMapper.writeValueAsString(user);

System.out.println(prettyJson);
    

[출력 결과]


{
  "name" : "김재능",
  "age" : 30,
  "email" : "jaenung@example.com"
}
    

와우! 훨씬 보기 편해졌지? 개발 및 디버깅 환경에서는 이 옵션을 켜두는 게 정신 건강에 아주 이로워. 😉
물론, 실제 서비스 환경에서는 불필요한 공백과 줄바꿈이 네트워크 트래픽을 미세하게나마 증가시킬 수 있으니 기본값(false)으로 두는 경우가 많아.

모르는 건 너그럽게: `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`

API를 연동하다 보면 이런 경우가 종종 발생해.
상대방 서버에서 보낸 JSON에는 `"address"`라는 필드가 있는데, 내 `User` 클래스에는 `address` 필드가 없는 거지.
이럴 때 Jackson의 기본 동작은 어떻게 될까?


ObjectMapper objectMapper = new ObjectMapper();
String jsonWithExtraField = "{\"name\":\"박코딩\",\"age\":28,\"address\":\"서울시 코딩구\"}";

// User 클래스에는 address 필드가 없으므로...
// com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException 예외 발생!
User user = objectMapper.readValue(jsonWithExtraField, User.class);
    

결과는? 예외 발생! 💣
Jackson은 기본적으로 매우 엄격해서, 매핑할 클래스에 존재하지 않는 속성이 JSON에 있으면 `UnrecognizedPropertyException`을 던져버려.
"이봐, `address`라는 건 내 `User` 클래스에 없는데? 이거 어떻게 처리해야 할지 모르겠어!" 라고 소리치는 거지.

하지만 대부분의 경우, 우리는 그냥 모르는 필드는 조용히 무시하고 나머지 아는 필드만 잘 채워주길 바라.
이때 사용하는 옵션이 바로 `FAIL_ON_UNKNOWN_PROPERTIES`야. 기본값이 `true`인데, 이걸 `false`로 바꿔주면 돼.


ObjectMapper objectMapper = new ObjectMapper();
// 모르는 속성이 있어도 실패하지 않도록 설정!
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

String jsonWithExtraField = "{\"name\":\"박코딩\",\"age\":28,\"address\":\"서울시 코딩구\"}";

// 이제 예외 없이 정상적으로 역직렬화 성공!
User user = objectMapper.readValue(jsonWithExtraField, User.class);

System.out.println(user); // User{name='박코딩', age=28, email='null'}
    

이제 `address` 필드는 깔끔하게 무시되고, `name`과 `age`만 정상적으로 매핑되었어.
외부 API와 연동할 때 이 옵션은 거의 필수라고 할 수 있을 정도로 자주 사용되니 꼭 기억해둬!

그 외 유용한 설정들 엿보기 🧐

`SerializationFeature.WRITE_DATES_AS_TIMESTAMPS`: `Date`나 `Calendar` 객체를 직렬화할 때, 기본값은 타임스탬프(숫자)로 변환해. 이걸 `false`로 설정하면 ISO-8601 형식의 문자열(`"1970-01-01T00:00:00.000+00:00"`)로 변환해줘. (더 최신인 Java 8의 `java.time` 패키지를 쓰려면 별도 모듈이 필요해. 나중에 다룰게!)

`MapperFeature.DEFAULT_VIEW_INCLUSION`: `@JsonView` 어노테이션을 사용할 때, 뷰가 지정되지 않은 속성을 포함할지 여부를 결정해. (이것도 고급 기능이라 뒤에서 자세히!)

`ObjectMapper` 설정은 이게 전부가 아니야. 수십 가지의 세밀한 옵션들이 더 있지만, 지금 소개한 두 가지만 알아도 웬만한 상황은 다 대처할 수 있을 거야.
하지만 "필드마다 다르게 동작하게 할 순 없을까?" 라는 생각이 든다면?
그럴 때 사용하는 것이 바로 다음 챕터의 주인공, 어노테이션(Annotation)이야!

4단계: 디테일의 마법, 어노테이션으로 JSON 조각하기 ✨

`ObjectMapper`의 전역 설정만으로는 해결하기 어려운 디테일한 요구사항들이 있어.
"이 필드만 JSON에서 다른 이름으로 쓰고 싶어.", "이 필드는 JSON으로 만들 때 빼고 싶어." 같은 것들 말이야.
이럴 때 우리는 클래스나 필드에 '꼬리표'를 붙여서 Jackson에게 특별한 지시를 내릴 수 있는데, 이 꼬리표가 바로 어노테이션이야.

Jackson 어노테이션을 사용하면 코드 레벨에서 아주 직관적으로 JSON의 모양을 조각할 수 있어.
마치 찰흙으로 작품을 만들 때 손가락으로 섬세하게 모양을 다듬는 것처럼 말이지.
자주 쓰이는 핵심 어노테이션들을 하나씩 정복해보자!

@JsonProperty: 내 이름은 이걸로 불러줘

Java에서는 변수명을 보통 카멜 케이스(camelCase)로 `userName`처럼 짓지만, JSON에서는 스네이크 케이스(snake_case)인 `user_name`을 선호하는 경우도 많아.
이렇게 객체의 필드명과 JSON의 키 이름을 다르게 매핑하고 싶을 때 `@JsonProperty`를 사용해.


public class Product {
    @JsonProperty("product_id") // 'productId' 필드를 JSON에서는 'product_id'로!
    private String productId;
    
    @JsonProperty("product_name")
    private String productName;

    private int price; // @JsonProperty가 없으면 필드명 'price' 그대로 사용됨

    // ... 생성자, getter/setter 생략 ...
}

// --- 직렬화 ---
Product product = new Product("P001", "노트북", 1500000);
String json = new ObjectMapper().writeValueAsString(product);
// 결과: {"price":1500000,"product_id":"P001","product_name":"노트북"}

// --- 역직렬화 ---
String inputJson = "{\"product_id\":\"P002\",\"product_name\":\"키보드\",\"price\":80000}";
Product product2 = new ObjectMapper().readValue(inputJson, Product.class);
// product2 객체에는 productId="P002", productName="키보드", price=80000 이 담김
    

`@JsonProperty` 하나로 Java 코드의 명명 규칙을 지키면서도, 외부 API의 JSON 명명 규칙을 따를 수 있게 됐어. 정말 유용하지?

@JsonIgnore: 넌 좀 빠져 있어

객체의 필드 중에서 JSON에 포함시키고 싶지 않은 민감한 정보(비밀번호 등)나 내부적으로만 사용하는 값이 있을 수 있어.
이럴 땐 `@JsonIgnore`를 붙여주면 직렬화, 역직렬화 과정에서 해당 필드를 투명인간처럼 취급해.


public class Member {
    private String id;
    
    @JsonIgnore // password 필드는 JSON 변환에서 제외!
    private String password;

    private String nickname;
    // ... 생성자, getter/setter 생략 ...
}

// --- 직렬화 ---
Member member = new Member("user01", "secret1234", "재능이");
String json = new ObjectMapper().writeValueAsString(member);
// 결과: {"id":"user01","nickname":"재능이"}  (password가 사라짐!)
    

`@JsonIgnore`는 클래스 전체에 적용할 필드가 많을 때 `@JsonIgnoreProperties({"field1", "field2"})` 형태로 클래스 레벨에 선언할 수도 있어.
앞서 봤던 `FAIL_ON_UNKNOWN_PROPERTIES=false` 설정과 비슷하게, 역직렬화 시 특정 필드를 무시하는 용도로도 사용할 수 있지.

@JsonInclude: 조건부로 보여줄게

`null` 값을 가진 필드는 굳이 JSON에 포함시키고 싶지 않을 때가 많아. JSON의 크기를 줄이고, 더 깔끔하게 보이기 때문이지.
`@JsonInclude` 어노테이션을 사용하면 이런 포함 조건을 아주 상세하게 제어할 수 있어.


import com.fasterxml.jackson.annotation.JsonInclude;

// 클래스 레벨에 선언하면 모든 필드에 적용됨
@JsonInclude(JsonInclude.Include.NON_NULL) 
public class Order {
    private String orderId;
    private String productName;
    private String shippingAddress; // 배송지 (null일 수 있음)
    // ... 생성자, getter/setter 생략 ...
}

// --- 직렬화 ---
Order order = new Order("O123", "마우스", null); // shippingAddress가 null
String json = new ObjectMapper().writeValueAsString(order);
// 결과: {"orderId":"O123","productName":"마우스"} (shippingAddress가 제외됨!)
    

`JsonInclude.Include`에는 다양한 옵션이 있어.

- NON_NULL: `null`이 아닌 경우에만 포함 (가장 많이 사용!)
- NON_EMPTY: `null`이 아니고, 빈 값(문자열은 `""`, 컬렉션은 `size()==0`)이 아닌 경우에만 포함
- NON_DEFAULT: `int`의 `0`, `boolean`의 `false` 같은 기본값이 아닌 경우에만 포함
- ALWAYS: 항상 포함 (기본값)

@JsonFormat: 날짜와 시간, 원하는 모양으로

Java의 `Date` 객체를 JSON으로 바꾸면 기본적으로 긴 숫자(타임스탬프)로 변환돼.
이걸 `"2023-10-27 10:30:00"`처럼 우리가 원하는 형식의 문자열로 바꾸고 싶을 때 `@JsonFormat`을 사용해.


import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;

public class Event {
    private String title;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
    private Date eventDate;
    // ... 생성자, getter/setter 생략 ...
}

// --- 직렬화 ---
Event event = new Event("재능넷 정기 세미나", new Date());
String json = new ObjectMapper().writeValueAsString(event);
// 결과: {"title":"재능넷 정기 세미나","eventDate":"2023-10-27 15:45:10"} (현재 시간에 따라)
    

`pattern`으로 원하는 날짜 형식을 지정하고, `timezone`까지 설정할 수 있어서 아주 편리해.

더 강력한 어노테이션들 맛보기 🍬

@JsonUnwrapped: 중첩된 객체를 풀어서 평평하게 만들고 싶을 때 사용해.
`{"id":1, "name":{"firstName":"길동", "lastName":"홍"}}` -> `@JsonUnwrapped` 사용 -> `{"id":1, "firstName":"길동", "lastName":"홍"}`

@JsonCreator + @JsonProperty: 불변(Immutable) 객체처럼 생성자를 통해 값을 주입해야 할 때 사용해. Jackson에게 "이 생성자를 사용해서 객체를 만들어줘!"라고 알려주는 거지.

@JsonView: 같은 객체라도 상황에 따라 다른 모습의 JSON을 만들고 싶을 때 사용해. 예를 들어, 일반 사용자에게는 일부 정보만 보여주고, 관리자에게는 모든 정보를 보여주는 시나리오에서 아주 유용해.

어때? 어노테이션의 세계는 정말 무궁무진하지?
이 어노테이션들만 잘 조합해서 사용해도 웬만한 JSON 데이터는 자유자재로 요리할 수 있게 될 거야.
하지만 여기서 만족하긴 일러! Jackson에는 더 깊고 강력한 기능들이 숨어있어.
다음 챕터에서는 정해진 클래스 없이 JSON을 다루는 방법과 상속 관계처럼 복잡한 구조를 처리하는 고급 기술을 탐험해볼게! 🚀

5단계: 고급 기능 탐험 - Jackson, 너의 한계는 어디? 🤯

지금까지 우리는 잘 정의된 Java 클래스(POJO)와 JSON을 1:1로 매핑하는 데이터 바인딩 모델을 주로 다뤘어.
하지만 실전에서는 POJO를 미리 정의하기 애매한 경우가 있어.
JSON 구조가 유동적이거나, 전체 데이터 중 특정 값 몇 개만 쏙 뽑아내고 싶을 때 말이야.
이럴 때를 위해 Jackson은 트리 모델(Tree Model)이라는 강력한 무기를 준비해뒀어.

트리 모델과 `JsonNode`: JSON을 지도처럼 탐색하기

트리 모델은 JSON 데이터를 이름 그대로 '트리(Tree)' 구조로 메모리에 읽어 들여, 각 노드(Node)를 자유롭게 탐색하는 방식이야.
이때 각 노드를 표현하는 클래스가 바로 JsonNode야.

`ObjectMapper`의 `readTree()` 메서드를 사용하면 JSON 문자열을 `JsonNode` 객체로 변환할 수 있어.


String jsonString = "{" +
    "\"id\": \"user01\"," +
    "\"name\": \"홍길동\"," +
    "\"isAdmin\": true," +
    "\"courses\": [\"Java\", \"Spring\", \"JPA\"]," +
    "\"profile\": {" +
        "\"email\": \"gildong@example.com\"," +
        "\"age\": 25" +
    "}" +
"}";

ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(jsonString);
    

이제 `rootNode`를 가지고 JSON 데이터를 자유롭게 여행할 수 있어!

- 특정 필드 값 가져오기: `get("fieldName")` 또는 `path("fieldName")`


String name = rootNode.get("name").asText(); // "홍길동"
boolean isAdmin = rootNode.get("isAdmin").asBoolean(); // true

// 중첩된 객체의 값 가져오기
int age = rootNode.get("profile").get("age").asInt(); // 25
    

- `get()` vs `path()`: 뭐가 다를까?

`get()`과 `path()`는 둘 다 필드를 찾는다는 점에서 비슷하지만, 결정적인 차이가 있어.
`get("없는필드")`는 `null`을 반환하는 반면, `path("없는필드")`는 `MissingNode`라는 특수한 객체를 반환해.
이게 왜 중요하냐면, `null`에 대고 `.get()`을 또 호출하면 `NullPointerException`이 터지지만, `MissingNode`에 대고 `.path()`를 호출하면 또 다른 `MissingNode`를 반환해서 연쇄적인 호출(Chaining)이 안전하다는 거야.


// get()을 사용한 위험한 체이닝
// 만약 "profile"이 없다면? get("profile")이 null이 되어 NullPointerException 발생!
// int age = rootNode.get("profile").get("age").asInt(); 

// path()를 사용한 안전한 체이닝
// "profile"이 없어도, "address"가 없어도 예외 없이 안전하게 진행됨
String street = rootNode.path("profile").path("address").path("street").asText("기본값"); // "기본값"
    

- 배열 다루기: `JsonNode`는 `Iterable`을 구현해서 `for-each` 루프로 순회할 수 있어.


JsonNode coursesNode = rootNode.get("courses");
if (coursesNode.isArray()) {
    for (JsonNode course : coursesNode) {
        System.out.println(course.asText()); // Java, Spring, JPA 출력
    }
}
    

트리 모델은 전체 구조를 클래스로 정의하지 않고도 필요한 데이터만 유연하게 추출할 수 있어서 데이터 분석이나 동적인 API 응답을 처리할 때 정말 강력해.

다형성 처리: 상속 관계의 객체를 우아하게

객체 지향 프로그래밍에서는 상속을 통해 'is-a' 관계를 표현하곤 해. 예를 들어, '개'는 '동물'이고 '고양이'도 '동물'이지.
이런 상속 구조를 가진 객체를 JSON으로 변환하고 다시 되돌리는 건 꽤 까다로운 문제야.

왜냐하면, `[{"name":"멍멍이"}, {"name":"야옹이"}]` 라는 JSON 배열을 `List`로 역직렬화하려고 할 때, Jackson은 첫 번째 객체를 `Dog`으로 만들어야 할지 `Cat`으로 만들어야 할지 알 수가 없기 때문이지.

이때 사용하는 것이 `@JsonTypeInfo`와 `@JsonSubTypes` 어노테이션이야.
JSON에 "이 객체는 사실 Dog 타입이야!"라는 타입 정보를 심어주는 역할을 해.


import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

// 부모 클래스
@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME, // 타입 정보를 이름으로 기록
    include = JsonTypeInfo.As.PROPERTY, // "type"이라는 새로운 프로퍼티로 기록
    property = "type" // 프로퍼티 이름은 "type"
)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "dog"), // "type":"dog" 이면 Dog 클래스로
    @JsonSubTypes.Type(value = Cat.class, name = "cat")  // "type":"cat" 이면 Cat 클래스로
})
abstract class Animal {
    public String name;
}

// 자식 클래스 1
class Dog extends Animal {
    public int barkVolume;
}

// 자식 클래스 2
class Cat extends Animal {
    public boolean likesCream;
}

// --- 직렬화 ---
List<Animal> animals = new ArrayList<>();
Dog dog = new Dog();
dog.name = "바둑이";
dog.barkVolume = 10;
Cat cat = new Cat();
cat.name = "나비";
cat.likesCream = true;
animals.add(dog);
animals.add(cat);

String json = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT).writeValueAsString(animals);
/*
[ {
  "type" : "dog",  // 타입 정보가 추가됨!
  "name" : "바둑이",
  "barkVolume" : 10
}, {
  "type" : "cat",  // 타입 정보가 추가됨!
  "name" : "나비",
  "likesCream" : true
} ]
*/

// --- 역직렬화 ---
List<Animal> deserializedAnimals = new ObjectMapper().readValue(json, new TypeReference<List<Animal>>() {});
// deserializedAnimals.get(0)은 Dog 인스턴스, get(1)은 Cat 인스턴스가 됨!
    

`@JsonTypeInfo`를 통해 타입 정보를 어떻게, 어디에 저장할지 정하고, `@JsonSubTypes`로 각 타입 이름에 어떤 클래스를 매핑할지 알려주면, Jackson이 직렬화할 때 타입 정보를 JSON에 심어주고, 역직렬화할 때 그 정보를 보고 정확한 자식 클래스의 객체를 만들어주는 거야. 정말 똑똑하지? 🤯

Jackson 모듈 시스템: 기능 확장하기

Jackson의 또 다른 강력함은 바로 '모듈(Module)' 시스템이야.
기본 `ObjectMapper`가 처리하지 못하는 특정 타입이나 기능을 처리하고 싶을 때, 해당 기능을 구현한 모듈을 `ObjectMapper`에 등록해서 기능을 확장할 수 있어.

가장 대표적인 예가 바로 Java 8에 도입된 새로운 날짜/시간 API(`java.time` 패키지의 `LocalDate`, `LocalDateTime` 등)를 처리하는 JavaTimeModule이야.


// JavaTimeModule 의존성 추가 (Maven/Gradle)
// com.fasterxml.jackson.datatype:jackson-datatype-jsr310

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.time.LocalDateTime;

public class Event {
    public String title;
    public LocalDateTime eventDateTime;
}

// --- 모듈 등록 및 사용 ---
ObjectMapper objectMapper = new ObjectMapper();
// JavaTimeModule을 등록!
objectMapper.registerModule(new JavaTimeModule());
// 날짜/시간 포맷을 타임스탬프 대신 ISO-8601 문자열로 변경
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Event event = new Event();
event.title = "Jackson 스터디";
event.eventDateTime = LocalDateTime.now();

String json = objectMapper.writeValueAsString(event);
// 결과: {"title":"Jackson 스터디","eventDateTime":"2023-10-27T16:30:55.123"}
    

모듈을 등록하지 않으면 `LocalDateTime`을 어떻게 직렬화해야 할지 몰라 에러가 발생해.
하지만 `JavaTimeModule`을 등록하는 순간, `ObjectMapper`는 `LocalDateTime`을 다루는 방법을 학습하게 되는 거지.

이 외에도 `Guava`, `Joda-Time`, `Kotlin` 등을 지원하는 다양한 공식/비공식 모듈들이 존재해.
필요하다면 직접 `JsonSerializer`나 `JsonDeserializer`를 구현해서 우리만의 커스텀 모듈을 만들 수도 있어. 그야말로 무한한 확장성을 자랑하지.

자, 이제 Jackson의 고급 기능까지 맛봤어. 이 정도면 당신은 이미 Jackson 준전문가!
하지만 진정한 고수는 이론뿐만 아니라 실전에서의 문제 해결 능력도 갖춰야겠지?
마지막 챕터에서는 현업에서 마주칠 수 있는 까다로운 문제들과 그 해결법, 그리고 성능을 위한 꿀팁들을 대방출할게!

6단계: 실전 팁 & 트러블슈팅 - 현장에서 살아남기 🪖

축하해! 이제 당신은 Jackson의 거의 모든 것을 알게 됐어. 하지만 이론과 실전은 다른 법!
현업에서는 예상치 못한 에러와 마주치고, 성능을 한계까지 쥐어짜야 하는 상황이 오기도 해.
이번 챕터에서는 실전에서 당신을 구해줄 꿀팁과 흔히 발생하는 문제들의 해결법을 알려줄게.

성능 최적화 팁: `ObjectMapper`는 비싸다! 재사용하라!

가장 중요하고, 또 가장 많이 하는 실수 중 하나야.
`ObjectMapper`를 사용할 때마다 `new ObjectMapper()`로 새로 생성하는 코드를 종종 볼 수 있어.


// 안티 패턴 ❌
public String convertToJson(Object data) throws JsonProcessingException {
    ObjectMapper mapper = new ObjectMapper(); // 매번 새로 생성
    return mapper.writeValueAsString(data);
}
    

이게 왜 문제일까? `ObjectMapper` 객체를 생성하는 비용은 생각보다 비싸.
내부적으로 수많은 Serializer, Deserializer를 찾고 캐싱하는 등 복잡한 초기화 과정을 거치기 때문이야.
요청이 많아질수록 이 초기화 비용이 누적되어 시스템 전체에 부담을 줄 수 있어.

해결책은 간단해. `ObjectMapper`는 스레드-안전(Thread-safe)하게 설계되었으니, 한 번만 만들어서 계속 재사용하면 돼!


// 추천 패턴 ✅
public class JsonUtil {
    // static final로 하나만 생성해서 공유
    private static final ObjectMapper MAPPER = new ObjectMapper();

    static {
        // 필요한 설정은 static 블록에서 한 번만!
        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        MAPPER.registerModule(new JavaTimeModule());
    }

    public static String toJson(Object data) throws JsonProcessingException {
        return MAPPER.writeValueAsString(data);
    }

    public static <T> T fromJson(String json, Class<T> clazz) throws JsonProcessingException {
        return MAPPER.readValue(json, clazz);
    }
}
    

이렇게 싱글톤(Singleton) 패턴이나 정적(static) 필드로 `ObjectMapper`를 관리하면, 비싼 초기화 비용은 애플리케이션 시작 시 단 한 번만 발생하고, 이후에는 계속 재사용되므로 성능상 큰 이점을 얻을 수 있어.
Spring 환경에서는 Bean으로 등록해서 주입받아 사용하면 자연스럽게 싱글톤으로 관리되니 더욱 편리하지.

단골 에러 #1: 무한 재귀(Infinite Recursion)의 늪

JPA/Hibernate 같은 ORM을 사용하다 보면 이 에러와 반드시 한 번은 마주치게 될 거야.
예를 들어, `Team`과 `Member`가 양방향 연관관계를 맺고 있는 상황을 생각해보자.


class Team {
    public String name;
    public List<Member> members;
}

class Member {
    public String name;
    public Team team;
}
    

이 상태에서 `Team` 객체를 직렬화하면 어떤 일이 벌어질까?

1. Jackson이 `Team`을 직렬화하다가 `members` 리스트를 만남.
2. `members` 리스트 안의 `Member` 객체를 직렬화하다가 `team` 필드를 만남.
3. `team` 필드는 다시 `Team` 객체이므로, 1번 과정으로 돌아가 `Team`을 직렬화 시작.
4. `Team` -> `Member` -> `Team` -> `Member` ... 핑퐁처럼 서로를 계속 호출하다가 결국 `StackOverflowError` 발생! 💥

이 무한 루프의 고리를 끊어주는 어노테이션이 바로 `@JsonManagedReference`와 `@JsonBackReference`야.


import com.fasterxml.jackson.annotation.JsonManagedReference;
import com.fasterxml.jackson.annotation.JsonBackReference;

class Team {
    public String name;
    
    @JsonManagedReference // 나는 관계의 주인(부모)이야. 직렬화에 포함시켜줘.
    public List<Member> members;
}

class Member {
    public String name;
    
    @JsonBackReference // 나는 관계의 일부(자식)야. 직렬화에서 나를 참조하는 부분은 생략해줘.
    public Team team;
}
    

이렇게 설정하면, `Team`을 직렬화할 때는 `members`가 정상적으로 포함되지만, `Member`를 직렬화할 때는 `team` 필드가 생략되어 무한 재귀가 발생하지 않아.
보통 컬렉션을 가진 쪽(OneToMany의 One)에 `@JsonManagedReference`를, 컬렉션의 요소가 되는 쪽(OneToMany의 Many)에 `@JsonBackReference`를 붙여주면 돼.

⚠️ `@JsonBackReference`의 함정

`@JsonBackReference`가 붙은 필드는 직렬화에서 아예 제외되기 때문에, `Member` 객체만 단독으로 JSON으로 만들면 `team` 정보가 사라져 버려.
만약 `Member`의 JSON에 `team`의 ID 같은 정보만이라도 포함시키고 싶다면, 이 방법 대신 DTO(Data Transfer Object)를 만들어서 필요한 데이터만 옮겨 담아 변환하는 것이 더 좋은 해결책일 수 있어.

단골 에러 #2: `UnrecognizedPropertyException` & `InvalidFormatException`

이 두 에러는 역직렬화 과정에서 자주 만나게 되는 친구들이야.

`UnrecognizedPropertyException`: 앞에서 잠깐 봤었지? JSON에는 있는데 매핑할 클래스에는 없는 필드가 있을 때 발생해.
- 해결책 1 (전역): `objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);`
- 해결책 2 (클래스 단위): 클래스 위에 `@JsonIgnoreProperties(ignoreUnknown = true)` 어노테이션 붙이기.

`InvalidFormatException`: 데이터 타입이 맞지 않을 때 발생해. 예를 들어, `int` 타입 필드에 `"abc"` 같은 문자열을 넣으려고 할 때.
- 해결책: 근본적으로는 API 스펙에 맞는 정확한 데이터를 보내는 것이 맞아. 하지만 예외적인 상황을 처리해야 한다면, 커스텀 Deserializer를 만들어서 특정 문자열을 기본값(예: `0`)으로 변환하도록 로직을 구현할 수도 있어.

Jackson + Spring Boot: 우린 이미 한 팀이야!

Spring Boot를 사용하고 있다면, 사실 당신은 이미 Jackson의 편리함을 누리고 있었어.
`spring-boot-starter-web` 의존성에는 `jackson-databind`가 기본적으로 포함되어 있어서, 별도 설정 없이도 Spring의 `@RestController`가 Java 객체를 반환하면 자동으로 JSON으로 변환해주는 마법이 바로 Jackson 덕분이야.

Spring Boot가 자동 설정해주는 `ObjectMapper`의 설정을 바꾸고 싶다면? `application.properties`나 `application.yml` 파일에서 간단하게 제어할 수 있어.

`application.properties` 예시


# 직렬화 시 들여쓰기 활성화
spring.jackson.serialization.indent-output=true

# 역직렬화 시 모르는 속성 있어도 에러 내지 않기
spring.jackson.deserialization.fail-on-unknown-properties=false

# 날짜 형식 지정
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=Asia/Seoul
    

만약 이런 프로퍼티 설정만으로 부족하다면, `@Configuration` 클래스에서 `Jackson2ObjectMapperBuilderCustomizer` Bean을 등록해서 프로그래밍 방식으로 `ObjectMapper`를 세밀하게 커스터마이징할 수도 있어.

혹시라도 프로젝트가 너무 복잡해져서 Jackson 커스터마이징이나 데이터 처리 로직에 전문가의 도움이 필요하다면, 재능넷 같은 플랫폼에서 숙련된 Java 개발자나 데이터 엔지니어의 도움을 받아보는 것도 좋은 방법이 될 수 있을 거야.

마무리하며: Jackson 마스터, 이제 당신의 이야기 🏆

와, 정말 긴 여정이었어! 👏
우리는 Jackson이라는 라이브러리를 설치하는 것부터 시작해서, `ObjectMapper`를 이용한 기본 변환, 어노테이션을 활용한 디테일한 제어, `JsonNode`를 이용한 동적 처리, 그리고 실전에서 마주칠 수 있는 문제 해결법까지... Jackson의 거의 모든 것을 훑어봤어.

처음엔 그저 'Java 객체를 JSON으로 바꿔주는 도구' 정도로만 생각했을지 몰라.
하지만 이제 당신은 Jackson이 얼마나 강력하고 유연하며, 또 섬세한 컨트롤이 가능한 라이브러리인지 알게 됐을 거야.
Jackson은 단순히 데이터를 변환하는 것을 넘어, 우리가 데이터를 더 '잘' 다룰 수 있도록 도와주는 든든한 파트너와도 같아.

핵심 요약, 이것만은 기억하자! ✅

1. `ObjectMapper`가 핵심이다: 직렬화는 `writeValueAsString()`, 역직렬화는 `readValue()`.
2. `ObjectMapper`는 재사용하라: 성능을 위해 `static final`이나 Bean으로 관리하자.
3. 어노테이션은 마법이다: `@JsonProperty`, `@JsonIgnore`, `@JsonInclude` 등은 디테일을 살리는 필수 스킬!
4. 양방향 관계는 조심하라: `@JsonManagedReference`, `@JsonBackReference`로 무한 재귀를 막자.
5. 모르는 건 너그럽게: `FAIL_ON_UNKNOWN_PROPERTIES`는 `false`로 설정하는 경우가 많다.
6. 기능 확장은 모듈로: `JavaTimeModule`처럼 필요한 모듈을 등록해서 `ObjectMapper`를 강화하자.

물론 이 글에서 다룬 내용이 Jackson의 100%는 아닐 거야. 더 깊이 들어가면 스트리밍 API를 직접 다루거나, 데이터 포맷 자체를 확장하는 등 훨씬 더 전문적인 영역도 존재해.
하지만 오늘 배운 내용만 완벽하게 당신의 것으로 만든다면, Java 개발자로서 마주치는 99%의 JSON 관련 이슈는 자신 있게 해결할 수 있을 거라고 확신해.

이제 남은 것은 직접 코드를 짜면서 Jackson과 더 친해지는 것뿐이야.
작은 토이 프로젝트든, 현재 진행 중인 실제 프로젝트든, 오늘 배운 기능들을 하나씩 적용해보며 그 편리함을 몸소 느껴봐.
에러와 마주치면 좌절하지 말고, 이 글을 다시 찾아보거나 공식 문서를 탐험하며 문제를 해결하는 과정을 즐겨봐. 그 과정 하나하나가 당신을 더 뛰어난 개발자로 만들어 줄 테니까.

JSON 데이터 앞에서 더 이상 작아지지 마.
당신 손에는 이미 Jackson이라는 강력한 무기가 들려있어.
이제 당신이 데이터를 지배할 차례야. Happy Coding! 💻✨

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.