1. Jackson 소개
Java 애플리케이션과 외부 세계 간 데이터 교환은 흔히 이렇게 요약할 수 있습니다: “Java 객체를 JSON으로 — 그리고 그 반대로 — 어떻게 바꿀 것인가?” 물론 String.split과 정규식으로 직접 파서를 작성할 수도 있습니다(고통 속에서 묘한 즐거움을 느낄 수도 있죠), 하지만 실제 프로젝트에서는 그렇게 하지 않습니다.
Java에서 JSON을 다루는 인기 있는 라이브러리가 몇 가지 있습니다. 그중 핵심은 Jackson입니다. 너무나 널리 쓰이기 때문에 Spring Boot에 기본 포함되어 있으며, 수많은 다른 프레임워크와 라이브러리에서도 사용됩니다.
Jackson이란?
Jackson은 직렬화(Java 객체를 JSON으로 변환)와 역직렬화(그 반대)를 위한 강력하고 유연한 라이브러리입니다. 여러 모듈로 구성되지만, 90%의 작업에는 두 가지만 알면 충분합니다:
- jackson-core — 코어, 저수준 파서.
- jackson-databind — 고수준 모듈로, Java 객체를 JSON으로 그리고 그 반대로 변환합니다.
시작할 때 알아둘 점 하나: ObjectMapper를 보면 그건 Jackson입니다.
Jackson이 Java에서 JSON을 다루는 사실상의 표준으로 여겨지는 이유는 단순함과 강력함을 겸비했기 때문입니다. 데이터를 직렬화하거나 역직렬화하려면 문자 그대로 몇 줄이면 충분하여 초보자에게도 매우 편리합니다. 동시에 애노테이션과 다양한 설정 덕분에 컬렉션, 중첩 엔티티, 다양한 형식의 날짜 등 데이터를 객체로 주고받는 과정을 세밀하게 제어할 수 있습니다.
또한 Jackson은 매우 빠르고 효율적으로 동작하여 대용량 데이터를 처리하는 실제 프로젝트에 중요합니다. 라이브러리 개발자들은 새로운 Java 버전과 최신 JSON 표준의 변화를 적극 반영합니다 — Jackson은 간단한 애플리케이션부터 대규모 엔터프라이즈 시스템까지 신뢰할 수 있는 선택지입니다.
2. JSON 읽기(역직렬화)
JSON 문자열을 읽어 Java 객체로 바꿔 봅시다. 이를 위해 필요한 것은:
- 데이터 클래스(예: Person)
- Jackson의 ObjectMapper 클래스
Jackson 추가
Maven을 사용한다면 pom.xml에 다음을 추가하세요:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
Gradle이라면 유사하게:
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
클래스 예시
public class Person {
public String name;
public int age;
}
참고: 단순화를 위해 필드를 public으로 두었습니다. 나중에 private 필드와 getter/setter 처리도 살펴봅니다.
JSON 예시
{
"name": "Alice",
"age": 30
}
역직렬화: JSON을 객체로 변환
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
String json = "{\"name\": \"Alice\", \"age\": 30}";
ObjectMapper mapper = new ObjectMapper();
Person person = mapper.readValue(json, Person.class);
System.out.println(person.name); // Alice
System.out.println(person.age); // 30
}
}
여기서 Jackson은 JSON 문자열을 파싱하여 클래스의 필드나 게터와 이름이 일치하는 필드를 찾아 readValue를 통해 해당 값을 객체에 채워 넣습니다.
객체 리스트 역직렬화
배열이 있다고 가정해 봅시다:
[
{ "name": "Bob", "age": 22 },
{ "name": "Eve", "age": 27 }
]
리스트로 역직렬화해 봅시다:
import com.fasterxml.jackson.core.type.TypeReference;
// ...
String json = "[{\"name\": \"Bob\", \"age\": 22}, {\"name\": \"Eve\", \"age\": 27}]";
ObjectMapper mapper = new ObjectMapper();
List<Person> people = mapper.readValue(json, new TypeReference<List<Person>>() {});
for (Person p : people) {
System.out.println(p.name + " (" + p.age + ")");
}
아마 이런 의문이 들 수 있습니다: 왜 그냥 mapper.readValue(json, List.class)라고 쓰면 안 될까요? 제네릭은 Java에서 컴파일 시 타입이 소거됩니다. 그래서 Jackson이 리스트 내부에 Person 타입의 객체가 들어 있음을 알 수 있도록 TypeReference가 필요합니다.
3. JSON 쓰기(직렬화)
이제 반대로 Java 객체를 JSON 문자열로 변환해 봅시다.
예시
ObjectMapper mapper = new ObjectMapper();
Person person = new Person();
person.name = "Charlie";
person.age = 40;
String json = mapper.writeValueAsString(person);
System.out.println(json);
// {"name":"Charlie","age":40}
객체 리스트 직렬화
ObjectMapper mapper = new ObjectMapper();
List<Person> people = new ArrayList<>();
people.add(new Person("Anna", 25));
people.add(new Person("Dmitry", 31));
String json = mapper.writeValueAsString(people);
System.out.println(json);
// [{"name":"Anna","age":25},{"name":"Dmitry","age":31}]
파일로 쓰기
ObjectMapper mapper = new ObjectMapper();
Person person = new Person();
person.name = "Charlie";
person.age = 40;
mapper.writeValue(new File("person.json"), person);
// person.json 파일에는 이제 JSON 객체가 들어 있습니다
읽기 좋은(pretty) JSON
기본적으로 Jackson은 모든 내용을 한 줄로 출력합니다. 하지만 사람에게 더 “읽기 좋은” 방식인 pretty printing을 사용할 수 있습니다. 이는 JSON 문자열을 들여쓰기, 줄바꿈, 정돈된 포맷으로 출력한다는 뜻입니다.
대부분의 경우 “일반” JSON은 압축을 위해 한 줄로 작성되지만, “읽기 좋은” 형식은 로그, 파일, 화면에서 데이터 구조를 쉽게 살펴보기 위해 사람을 위한 용도로 사용됩니다.
ObjectMapper mapper = new ObjectMapper();
Person person = new Person();
person.name = "Charlie";
person.age = 40;
String prettyJson = mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(person);
System.out.println(prettyJson);
/*
{
"name" : "Charlie",
"age" : 40
}
*/
4. Jackson 애노테이션
Jackson은 직렬화와 역직렬화 과정을 제어하는 다양한 애노테이션을 지원합니다. 다음이 가장 유용합니다:
@JsonProperty
클래스의 필드 이름과 다른 JSON 필드 이름을 지정할 수 있습니다.
import com.fasterxml.jackson.annotation.JsonProperty;
public class Person {
@JsonProperty("full_name")
public String name;
public int age;
}
{"full_name": "Olga", "age": 28}
Jackson은 JSON의 full_name 필드를 객체의 name 필드로 매핑합니다.
@JsonIgnore
특정 필드를 직렬화하거나 역직렬화하지 않으려면:
import com.fasterxml.jackson.annotation.JsonIgnore;
public class Person {
public String name;
@JsonIgnore
public int age;
}
객체에 존재하더라도 JSON에는 age 필드가 포함되지 않습니다.
@JsonInclude
어떤 필드가 JSON에 포함될지 제어합니다. 예를 들어, null이 아닌 필드만 직렬화하려면:
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Person {
public String name;
public Integer age;
}
age == null이면 JSON에 "age" 키가 생성되지 않습니다.
@JsonFormat
날짜와 시간의 직렬화 형식을 지정할 수 있습니다.
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;
public class Event {
public String title;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
public Date date;
}
Event event = new Event();
event.title = "Hackathon";
event.date = new Date();
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(event);
// {"title":"Hackathon","date":"2024-06-07 15:23:00"}
예시: 모두 합치기
import com.fasterxml.jackson.annotation.*;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Person {
@JsonProperty("full_name")
private String name;
private int age;
@JsonIgnore
private String password;
// private 필드에는 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; }
}
5. 실습: 애노테이션을 고려한 직렬화와 역직렬화
연습용 애플리케이션을 확장해 봅시다 — 이제 User 클래스가 있고, private 필드와 등록일, 그리고 JSON에 포함되면 안 되는 비밀번호가 있습니다.
import com.fasterxml.jackson.annotation.*;
import java.util.Date;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
@JsonProperty("login")
private String username;
private int age;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private Date registered;
@JsonIgnore
private String password;
// getter와 setter는 필수!
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public Date getRegistered() { return registered; }
public void setRegistered(Date registered) { this.registered = registered; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
직렬화
import com.fasterxml.jackson.databind.ObjectMapper;
import java.text.SimpleDateFormat;
ObjectMapper mapper = new ObjectMapper();
User user = new User();
user.setUsername("superuser");
user.setAge(42);
user.setRegistered(new SimpleDateFormat("yyyy-MM-dd").parse("2024-06-07"));
user.setPassword("qwerty123"); // JSON에 포함되지 않습니다!
String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);
System.out.println(json);
/*
{
"login" : "superuser",
"age" : 42,
"registered" : "2024-06-07"
}
*/
역직렬화
import com.fasterxml.jackson.databind.ObjectMapper;
ObjectMapper mapper = new ObjectMapper();
String json = "{ \"login\": \"superuser\", \"age\": 42, \"registered\": \"2024-06-07\" }";
User user = mapper.readValue(json, User.class);
System.out.println(user.getUsername()); // superuser
System.out.println(user.getAge()); // 42
System.out.println(user.getRegistered());// Fri Jun 07 00:00:00 ...
System.out.println(user.getPassword()); // null (이게 바람직합니다!)
6. Jackson 사용 시 흔한 오류
오류 №1: 매개변수 없는 생성자가 없음.
클래스에 매개변수 없는 생성자가 없으면 Jackson은 해당 클래스의 객체를 만들 수 없습니다. 이는 보통 인자가 있는 생성자만 명시했을 때 발생합니다.
오류 №2: private 필드에 getter/setter가 없음.
모든 필드를 private으로 만들고 getter와 setter를 추가하지 않으면(기본 설정에서) Jackson은 역직렬화 시 필드를 채울 수 없습니다.
오류 №3: 필드 이름 불일치.
JSON의 필드 이름이 Java의 필드/게터 이름과 다르면 Jackson이 매핑을 찾지 못합니다. @JsonProperty를 사용하세요.
오류 №4: 잘못된 날짜 형식.
JSON의 날짜 형식이 Java에서 기대하는 형식과 다르면 Jackson이 파싱 오류를 냅니다. @JsonFormat으로 형식을 설정하세요.
오류 №5: @JsonIgnore가 붙은 필드를 직렬화하려 함.
이런 필드는 JSON에 포함되지 않습니다 — 버그가 아니라 의도된 동작입니다.
오류 №6: 타입을 지정하지 않은 컬렉션의 직렬화/역직렬화.
TypeReference를 사용하지 않으면 Jackson은 컬렉션 내부 객체의 타입을 알 수 없습니다.
오류 №7: 파일 읽기/쓰기 중 예외.
파일 작업 시 IOException 처리를 잊지 마세요.
GO TO FULL VERSION