ObjectMapper에 대해 알아보자

이윤설·2024년 6월 9일
1

개요

Jackson 라이브러리의 ObjectMapper란, Java 객체를 JSON으로 또는 반대로 JSON을 Java 객체로 역직렬화하는 도구이다.
(주로 JSON에 대해 사용되지만 추가 모듈을 설치하면 XML, CSV 등 다른 데이터 형식도 처리할 수 있다.)

자주 쓰이므로 꼭 알아두자.

  • Jackson Libarary
    Jackson은 Java용 JSON 처리 라이브러리로, 널리 사용되는 오픈소스 프로젝트다.

직렬화와 역직렬화

우선 프로그래밍의 관점에서 "직렬화가 역직렬화"가 무엇인지 되짚어보자.

직렬화)
정의: 객체의 상태를 바이트 스트림(ex.JSON)으로 변환하는 과정이다. 이 바이트 스트림은 파일, 데이터베이스, 메모리, 네트워크 등 다양한 매체에 저장되거나 전송될 수 있다.
ex. Java 객체 -> JSON

용도
영구 저장: 객체를 파일이나 데이터베이스에 저장하여 나중에 복원할 수 있다.
네트워크 전송: 객체를 네트워크를 통해 다른 시스템으로 전송할 수 있다.
캐시: 객체를 캐시에 저장하여 빠르게 접근할 수 있다.

역직렬화)
정의: 바이트 스트림을 다시 객체로 변환하는 과정이다. 직렬화된 데이터를 읽어들여 원래 객체의 상태를 복원한다.
ex. JSON -> Java 객체

용도
데이터 복원: 파일이나 데이터베이스에 저장된 객체를 다시 메모리로 불러올 수 있다.
네트워크 데이터 수신: 네트워크를 통해 받은 데이터를 객체로 변환할 수 있다.

cf. JSON과 바이트스트림
JSON 자체는 바이트 스트림이 아니지만, JSON 데이터를 저장하거나 전송할 때는 바이트 스트림 형태로 처리된다.

기본 기능

Java object to JSON

public class Burger {
    private String name;
    private int pattyCount;
    private double price;

    public Burger() {}

    public Burger(String name, int pattyCount, double price) {
        this.name = name;
        this.pattyCount = pattyCount;
        this.price = price;
    }

    // Getter 및 Setter 메서드
}

public class Main {
    public static void main(String[] args) {
        try {
            ObjectMapper objectMapper = new ObjectMapper();

            Burger burger = new Burger("Classic Burger", 2, 5.99);

            // 햄버거 객체를 JSON 문자열로 변환
            String jsonString = objectMapper.writeValueAsString(burger);

            // JSON 문자열 출력
            System.out.println(jsonString);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

출력:
{
"name": "Classic Burger",
"pattyCount": 2,
"price": 5.99
}

objectMapper.writeValueAsString()에 JSON으로 변환하고자 하는 자바 객체를 넣으니 너무나도 쉽게 JSON으로 변환 되었다.

JSON to Java Object

public class Spaghetti {
    private String name;
    private String type;
    private boolean hasMeatballs;
    
    public Spaghetti() {}

    public Spaghetti(String name, String type, boolean hasMeatballs) {
        this.name = name;
        this.type = type;
        this.hasMeatballs = hasMeatballs;
    }

    // Getter 및 Setter 메서드

    @Override
    public String toString() {
        return "Spaghetti{" +
                "name='" + name + '\'' +
                ", type='" + type + '\'' +
                ", hasMeatballs=" + hasMeatballs +
                '}';
    }
}

public class Main {
    public static void main(String[] args) {
        try {
            // JSON 문자열
            String jsonString = "{\"name\":\"Carbonara\",\"type\":\"Cream\",\"hasMeatballs\":false}";

            // ObjectMapper 인스턴스 생성
            ObjectMapper objectMapper = new ObjectMapper();

            // JSON 문자열을 Spaghetti 객체로 변환
            Spaghetti spaghetti = objectMapper.readValue(jsonString, Spaghetti.class);

            // 변환된 객체 출력
            System.out.println(spaghetti);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

출력: Spaghetti{name='Carbonara', type='Cream', hasMeatballs=false}

이번에도 정말 쉽게 자바 객체가 JSON으로 변환되었다.

JSON to Jackson JsonNode

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Main {
    public static void main(String[] args) {
        try {
            // JSON 문자열
            String jsonString = "{\"name\":\"Carbonara\",\"type\":\"Cream\",\"hasMeatballs\":false}";

            // ObjectMapper 인스턴스 생성
            ObjectMapper objectMapper = new ObjectMapper();

            // JSON 문자열을 JsonNode 객체로 변환
            JsonNode rootNode = objectMapper.readTree(jsonString);

            // JsonNode에서 데이터 추출
            String name = rootNode.get("name").asText();
            String type = rootNode.get("type").asText();
            boolean hasMeatballs = rootNode.get("hasMeatballs").asBoolean();

            // 추출한 데이터 출력
            System.out.println("Name: " + name);
            System.out.println("Type: " + type);
            System.out.println("Has Meatballs: " + hasMeatballs);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

출력:
Name: Carbonara
Type: Cream
Has Meatballs: false

JsonNode

JsonNode는 Jackson 라이브러리에서 제공하는 클래스 중 하나로, JSON 데이터를 트리 구조로 표현하는 데 사용된다. JSON 데이터의 특정 요소에 접근하거나 조작하는 기능을 제공한다.

get(String fieldName): 지정된 필드 이름의 값을 반환한다.
asText(), asInt(), asBoolean(), asDouble(): 노드의 값을 다양한 타입으로 반환한다.
put(String fieldName, String value): JSON 객체의 필드를 업데이트한다.
toPrettyString(): JSON 트리 구조를 보기 좋은 형식으로 문자열로 변환한다.

Creating a Java List From a JSON Array String

[
  {"name": "Spaghetti Carbonara", "type": "Cream", "hasMeatballs": false},
  {"name": "Spaghetti Bolognese", "type": "Tomato", "hasMeatballs": true},
  {"name": "Spaghetti Aglio e Olio", "type": "Oil", "hasMeatballs": false}
]

위와 같은 JSON을 배열로 만들수도 있다.

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.List;

public class Main {
    public static void main(String[] args) {
        try {
            // JSON 배열 문자열
            String jsonArrayString = """
                [
                  {"name": "Spaghetti Carbonara", "type": "Cream", "hasMeatballs": false},
                  {"name": "Spaghetti Bolognese", "type": "Tomato", "hasMeatballs": true},
                  {"name": "Spaghetti Aglio e Olio", "type": "Oil", "hasMeatballs": false}
                ]
                """;

            // ObjectMapper 인스턴스 생성
            ObjectMapper objectMapper = new ObjectMapper();

            // JSON 배열 문자열을 Java 리스트로 변환
            List<Spaghetti> spaghettiList = objectMapper.readValue(jsonArrayString, new TypeReference<List<Spaghetti>>(){});

            // 변환된 리스트 출력
            for (Spaghetti spaghetti : spaghettiList) {
                System.out.println(spaghetti);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

출력:
Spaghetti{name='Spaghetti Carbonara', type='Cream', hasMeatballs=false}
Spaghetti{name='Spaghetti Bolognese', type='Tomato', hasMeatballs=true}
Spaghetti{name='Spaghetti Aglio e Olio', type='Oil', hasMeatballs=false}

TypeGenerator

// JSON 배열 문자열을 Java 리스트로 변환
List<Spaghetti> spaghettiList = objectMapper.readValue(jsonArrayString, new TypeReference<List<Spaghetti>>(){});

JSON을 리스트로 변환할 때 TypeReference라는 것이 List를 감싼다.
결론부터 얘기하면, new TypeReference<List>() {}는 JSON 데이터를 제네릭 타입으로 변환할 때 사용하는 방법이다.

  • 문제 배경
    Java의 제네릭(Generics) 타입은 컴파일 시간에만 사용되고, 런타임에는 타입 정보가 사라진다. 이 때문에 JSON 데이터를 제네릭 타입으로 변환할 때 문제가 생길 수 있다. (타입 소거)
  • 해결 방법: TypeReference
    Jackson 라이브러리는 이 문제를 해결하기 위해 TypeReference라는 클래스를 제공한다. TypeReference를 사용하면 제네릭 타입 정보를 유지할 수 있다.

🍔 TypeReference 상속: TypeReference<List>를 익명 클래스로 상속하여, 내부적으로 제네릭 타입 정보를 유지한다.
🍔 제네릭 타입 캡처: Jackson은 TypeReference를 사용하여 런타임에 제네릭 타입 정보를 캡처하고, JSON 데이터를 해당 타입으로 변환한다.

Creating Java Map From JSON String

{
  "name": "Margherita",
  "size": "Medium",
  "price": 12.99,
  "isVegetarian": true,
  "toppings": ["Tomato", "Mozzarella", "Basil"]
}  

이 JSON을 Map으로도 변환할 수 있다. 어렵지 않다.

public class Main {
    public static void main(String[] args) {
        try {
            // JSON 문자열
            String jsonString = """
                {
                  "name": "Margherita",
                  "size": "Medium",
                  "price": 12.99,
                  "isVegetarian": true,
                  "toppings": ["Tomato", "Mozzarella", "Basil"]
                }
                """;

            // ObjectMapper 인스턴스 생성
            ObjectMapper objectMapper = new ObjectMapper();

            // JSON 문자열을 Java Map으로 변환
            Map<String, Object> pizzaMap = objectMapper.readValue(jsonString, Map.class);

            // 변환된 Map 출력
            System.out.println("Pizza Map: " + pizzaMap);

            // 각 키에 해당하는 값 출력
            System.out.println("Name: " + pizzaMap.get("name"));
            System.out.println("Size: " + pizzaMap.get("size"));
            System.out.println("Price: " + pizzaMap.get("price"));
            System.out.println("Is Vegetarian: " + pizzaMap.get("isVegetarian"));
            System.out.println("Toppings: " + pizzaMap.get("toppings"));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

출력:
Pizza Map: {name=Margherita, size=Medium, price=12.99, isVegetarian=true, toppings=[Tomato, Mozzarella, Basil]}
Name: Margherita
Size: Medium
Price: 12.99
Is Vegetarian: true
Toppings: [Tomato, Mozzarella, Basil]


고급 기능

Jackson 라이브러리의 가장 강력한 기능 중 하나는 커스텀 직렬화/역직렬화 기능이다.

Configuring Serialization or Deserialization Feature

  {
  "name": "Cheeseburger",
  "bunType": "Sesame",
  "hasCheese": true,
  "pattyCount": 2,
  "price": 8.99,
  "extraField": "This field is not in the Burger class"
}
-----------------------------------------------
public class Burger {
    private String name;
    private String bunType;
    private boolean hasCheese;
    private int pattyCount;
    private double price;

    public Burger() {}

    public Burger(String name, String bunType, boolean hasCheese, int pattyCount, double price) {
        this.name = name;
        this.bunType = bunType;
        this.hasCheese = hasCheese;
        this.pattyCount = pattyCount;
        this.price = price;
    }

    // Getter 및 Setter 메서드

    @Override
    public String toString() {
        return "Burger{" +
                "name='" + name + '\'' +
                ", bunType='" + bunType + '\'' +
                ", hasCheese=" + hasCheese +
                ", pattyCount=" + pattyCount +
                ", price=" + price +
                '}';
    }
}

만약 맨 위에 있는 JSON을 Burger 클래스에 매핑하려고 하면 UnrecognizedPropertyException이 발생한다. 왜냐하면 extraField는 Burger에 없기 때문에 정상적으로 매핑할 수 없기 때문이다.

이러한 경우를 방지하려면, configure()라는 메소드를 사용하면 된다.

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Main {
    public static void main(String[] args) {
        try {
            // JSON 문자열
            String jsonString = """
                {
                  "name": "Cheeseburger",
                  "bunType": "Sesame",
                  "hasCheese": true,
                  "pattyCount": 2,
                  "price": 8.99,
                  "extraField": "This field is not in the Burger class"
                }
                """;

            // ObjectMapper 인스턴스 생성
            ObjectMapper objectMapper = new ObjectMapper();

            // 새로운 필드가 있을 경우 예외가 발생하지 않도록 설정
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

            // null 값이 원시 값에 허용되지 않도록 설정
            objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true);

            // enum 값이 숫자로 직렬화/역직렬화되지 않도록 설정
            objectMapper.configure(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, true);

            // JSON 문자열을 Burger 객체로 변환
            Burger burger = objectMapper.readValue(jsonString, Burger.class);

            // 변환된 객체 출력
            System.out.println(burger);

            // JsonNode를 사용하여 특정 필드 추출
            JsonNode jsonNodeRoot = objectMapper.readTree(jsonString);
            JsonNode jsonNodeExtraField = jsonNodeRoot.get("extraField");
            String extraField = jsonNodeExtraField != null ? jsonNodeExtraField.asText() : null;
            System.out.println("Extra Field: " + extraField);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

출력:
Burger{name='Cheeseburger', bunType='Sesame', hasCheese=true, pattyCount=2, price=8.99}
Extra Field: This field is not in the Burger class

objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

라는 코드를 통해 예외가 발생하지 않고 잘 출력이 되었다.

뿐만 아니라 null 값이 원시 값에 허용되지 않거나, enum 값이 숫자로 직렬화/역직렬화되지 않도록 설정할 수도 있다.

Creating Custom Serializer or Deserializer

기본 ObjectMapper가 아닌 커스텀 직렬화기/역직렬화기를 만들어서 사용해야하는 경우도 발생할 수 있다.

  1. JSON 필드 이름이 Java 객체와 다를 때
    문제:
    외부 시스템과 통신할 때 JSON 필드 이름이 Java 객체의 필드 이름과 다른 경우

해결:
커스텀 직렬화기와 역직렬화기를 사용하여 JSON 필드 이름을 Java 객체 필드 이름과 매핑할 수 있다.

  1. 특정 형식의 변환이 필요할 때
    문제:
    JSON 데이터에서 특정 형식을 다른 형식으로 변환해야 할 때가 있다. 예를 들어, 날짜 형식이 특정 포맷으로 되어 있는 경우.

해결:
커스텀 직렬화기와 역직렬화기를 사용하여 날짜 형식을 원하는 형식으로 변환할 수 있다.

  1. 조건부 직렬화/역직렬화가 필요할 때
    문제:
    조건에 따라 JSON 데이터의 일부만 직렬화하거나 역직렬화해야 할 때가 있다. 예를 들어, 특정 조건을 만족하는 필드만 직렬화하거나 역직렬화하고 싶을 때.

해결:
커스텀 직렬화기와 역직렬화기를 사용하여 조건에 따라 JSON 데이터를 처리할 수 있다.

  1. JSON 구조가 복잡할 때
    문제:
    JSON 구조가 복잡하거나 중첩된 경우 기본 ObjectMapper로는 처리하기 어려울 수 있다.

해결:
커스텀 직렬화기와 역직렬화기를 사용하여 복잡한 JSON 구조를 원하는 Java 객체로 변환할 수 있다.

public class CustomBurgerSerializer extends StdSerializer<Burger> {
    public CustomBurgerSerializer() {
        this(null);
    }

    public CustomBurgerSerializer(Class<Burger> t) {
        super(t);
    }

    @Override
    public void serialize(Burger burger, JsonGenerator jsonGenerator, SerializerProvider serializer) throws IOException {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeStringField("burger_name", burger.getName());
        jsonGenerator.writeStringField("burger_bun", burger.getBunType());
        jsonGenerator.writeBooleanField("has_cheese", burger.isHasCheese());
        jsonGenerator.writeNumberField("patty_count", burger.getPattyCount());
        jsonGenerator.writeNumberField("burger_price", burger.getPrice());
        jsonGenerator.writeEndObject();
    }
}

public class CustomBurgerDeserializer extends StdDeserializer<Burger> {
    public CustomBurgerDeserializer() {
        this(null);
    }

    public CustomBurgerDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public Burger deserialize(JsonParser parser, DeserializationContext deserializer) throws IOException {
        ObjectCodec codec = parser.getCodec();
        JsonNode node = codec.readTree(parser);

        String name = node.get("burger_name").asText();
        String bunType = node.get("burger_bun").asText();
        boolean hasCheese = node.get("has_cheese").asBoolean();
        int pattyCount = node.get("patty_count").asInt();
        double price = node.get("burger_price").asDouble();

        return new Burger(name, bunType, hasCheese, pattyCount, price);
    }
}
public class Main {
    public static void main(String[] args) {
        try {
            // JSON 문자열
            String jsonString = """
                {
                  "burger_name": "Cheeseburger",
                  "burger_bun": "Sesame",
                  "has_cheese": true,
                  "patty_count": 2,
                  "burger_price": 8.99
                }
                """;

            // ObjectMapper 인스턴스 생성
            ObjectMapper mapper = new ObjectMapper();

            // 커스텀 직렬화기 및 역직렬화기 모듈 등록
            SimpleModule module = new SimpleModule("CustomBurgerModule", new com.fasterxml.jackson.core.Version(1, 0, 0, null, null, null));
            module.addSerializer(Burger.class, new CustomBurgerSerializer());
            module.addDeserializer(Burger.class, new CustomBurgerDeserializer());
            mapper.registerModule(module);

            // JSON 문자열을 Burger 객체로 역직렬화
            Burger burger = mapper.readValue(jsonString, Burger.class);
            System.out.println(burger);

            // 객체를 JSON 문자열로 직렬화
            String burgerJson = mapper.writeValueAsString(burger);
            System.out.println(burgerJson);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Handling Date Formats

Jackson 라이브러리를 사용하여 java.util.Date 객체를 특정 형식의 문자열로 직렬화할 수있다.
기본적으로 java.util.Date 객체는 에포크 타임스탬프(1970년 1월 1일 이후의 밀리초 수)로 직렬화된다.
하지만 이는 사람이 읽기 어려우므로, 특정 형식의 문자열로 변환할 필요가 있다.

public class Milkshake {
    private String flavor;
    private double price;

    public Milkshake() {}

    public Milkshake(String flavor, double price) {
        this.flavor = flavor;
        this.price = price;
    }

    public String getFlavor() {
        return flavor;
    }

    public void setFlavor(String flavor) {
        this.flavor = flavor;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Milkshake{" +
                "flavor='" + flavor + '\'' +
                ", price=" + price +
                '}';
    }
}
import java.util.Date;

public class Request {
    private Milkshake milkshake;
    private Date datePurchased;

    public Request() {}

    public Request(Milkshake milkshake, Date datePurchased) {
        this.milkshake = milkshake;
        this.datePurchased = datePurchased;
    }

    public Milkshake getMilkshake() {
        return milkshake;
    }

    public void setMilkshake(Milkshake milkshake) {
        this.milkshake = milkshake;
    }

    public Date getDatePurchased() {
        return datePurchased;
    }

    public void setDatePurchased(Date datePurchased) {
        this.datePurchased = datePurchased;
    }

    @Override
    public String toString() {
        return "Request{" +
                "milkshake=" + milkshake +
                ", datePurchased=" + datePurchased +
                '}';
    }
}

import com.fasterxml.jackson.databind.ObjectMapper;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class Main {
    public static void main(String[] args) {
        try {
            // 밀크쉐이크 객체 생성
            Milkshake milkshake = new Milkshake("Chocolate", 4.99);

            // 현재 날짜와 시간
            Date datePurchased = new Date();

            // Request 객체 생성
            Request request = new Request(milkshake, datePurchased);

            // ObjectMapper 인스턴스 생성
            ObjectMapper objectMapper = new ObjectMapper();

            // 날짜 형식을 설정
            DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm a z", Locale.ENGLISH);
            objectMapper.setDateFormat(df);

            // Request 객체를 JSON 문자열로 직렬화
            String requestJson = objectMapper.writeValueAsString(request);

            // JSON 문자열 출력
            System.out.println(requestJson);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

출력:
{"milkshake":{"flavor":"Chocolate","price":4.99},"datePurchased":"2023-07-03 11:43 AM CEST"}

Handling Collections

배열

배열 또는 리스트로 변환할 수 있다.

[
  { "flavor": "Chocolate", "price": 4.99 },
  { "flavor": "Vanilla", "price": 3.99 }
]

이러한 JSON을 배열로 변환해보자.

public class Milkshake {
    private String flavor;
    private double price;

    public Milkshake() {}

    public Milkshake(String flavor, double price) {
        this.flavor = flavor;
        this.price = price;
    }

	// getter, setter

    @Override
    public String toString() {
        return "Milkshake{" +
                "flavor='" + flavor + '\'' +
                ", price=" + price +
                '}';
    }
}
public class Main {
    public static void main(String[] args) {
        try {
            // JSON 배열 문자열
            String jsonMilkshakeArray = """
                [
                  { "flavor": "Chocolate", "price": 4.99 },
                  { "flavor": "Vanilla", "price": 3.99 }
                ]
                """;

            // ObjectMapper 인스턴스 생성
            ObjectMapper objectMapper = new ObjectMapper();

            // JSON 배열을 Java 배열로 변환
            objectMapper.configure(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY, true);
            Milkshake[] milkshakes = objectMapper.readValue(jsonMilkshakeArray, Milkshake[].class);

            // 변환된 배열 출력
            for (Milkshake milkshake : milkshakes) {
                System.out.println(milkshake);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

리스트

TypeReference를 이용하면 쉽다.
JSON을 리스트로 변환하는 과정은 위에서 설명했으므로 생략하겠다.

Builder Pattern for ObjectMapper

5.1. ObjectMapperBuilder Class

ObjectMapper의 불변 인스턴스를 생성하기 위해 ObjectMapperBuilder 클래스를 프로토타입으로 작성한다.
먼저, 몇 가지 구성 매개변수를 사용하여 ObjectMapperBuilder 클래스를 만듭니다. 여기서는 enableIndentation, preserveOrder, dateFormat을 사용합니다.

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.util.TimeZone;

public class ObjectMapperBuilder {
    private boolean enableIndentation;
    private boolean preserveOrder;
    private DateFormat dateFormat;

    public ObjectMapperBuilder enableIndentation() {
        this.enableIndentation = true;
        return this;
    }

    public ObjectMapperBuilder dateFormat() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm a z");
        simpleDateFormat.setTimeZone(TimeZone.getTimeZone(ZoneId.of("Asia/Kolkata")));
        this.dateFormat = simpleDateFormat;
        return this;
    }

    public ObjectMapperBuilder preserveOrder(boolean order) {
        this.preserveOrder = order;
        return this;
    }

    public ObjectMapper build() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.enableIndentation);
        objectMapper.setDateFormat(this.dateFormat);
        if (this.preserveOrder) {
            objectMapper.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS);
        }
        return objectMapper;
    }
}

5.2 Builder 사용 예제

public class Fries {
    private String type;
    private double price;

    public Fries() {}

    public Fries(String type, double price) {
        this.type = type;
        this.price = price;
    }

	// getter setter

    @Override
    public String toString() {
        return "Fries{" +
                "type='" + type + '\'' +
                ", price=" + price +
                '}';
    }
}

import java.util.Date;

public class Request {
    private Fries fries;
    private Date datePurchased;

    public Request() {}

	// getter setter

    @Override
    public String toString() {
        return "Request{" +
                "fries=" + fries +
                ", datePurchased=" + datePurchased +
                '}';
    }
}
public class Main {
    public static void main(String[] args) {
        try {
            // ObjectMapper 인스턴스 생성
            ObjectMapper mapper = new ObjectMapperBuilder()
                    .enableIndentation()
                    .dateFormat()
                    .preserveOrder(true)
                    .build();

            // Fries 객체 생성 및 JSON 문자열로 직렬화
            Fries givenFries = new Fries("Curly", 3.99);
            String givenFriesJsonStr = "{ \"type\" : \"Curly\", \"price\" : 3.99 }";

            // JSON 문자열을 Fries 객체로 역직렬화
            Fries actualFries = mapper.readValue(givenFriesJsonStr, Fries.class);
            System.out.println("역직렬화된 Fries 객체: " + actualFries);

            // Request 객체 생성 및 설정
            Request request = new Request();
            request.setFries(givenFries);
            Date date = new Date(1684909857000L); // 특정 날짜 설정
            request.setDatePurchased(date);

            // Request 객체를 JSON 문자열로 직렬화
            String actualJson = mapper.writeValueAsString(request);
            System.out.println("직렬화된 JSON 문자열:\n" + actualJson);

            // 예상 JSON 문자열
            String expectedJson = "{\n" + 
                    "  \"fries\" : {\n" + 
                    "    \"type\" : \"Curly\",\n" + 
                    "    \"price\" : 3.99\n" + 
                    "  },\n" + 
                    "  \"datePurchased\" : \"2023-05-24 12:00 PM IST\"\n" + 
                    "}";

            // 출력된 JSON 문자열이 예상 문자열과 일치하는지 확인
            System.out.println("예상 JSON 문자열:\n" + expectedJson);
            System.out.println("직렬화 결과 일치 여부: " + expectedJson.equals(actualJson));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

비교

그럼 어떤 경우에 builder 패턴으로 objectmapper를 사용하는 것이 좋고, 또 장단점은 무엇일까?

  • 빌더 패턴을 사용해야 하는 경우 및 장점

    복잡한 구성: ObjectMapper를 여러 가지 방식으로 구성해야 하는 경우, 빌더 패턴을 사용하면 더 읽기 쉽고 관리하기 쉬운 코드를 작성할 수 있다. 예를 들어, 여러 개의 설정을 순차적으로 적용해야 할 때 유용하다.

    가독성 향상: 메서드 체이닝을 통해 설정을 적용할 수 있으므로, 구성 코드의 가독성이 향상된다.

    불변성 유지: 빌더 패턴을 사용하면 불변 객체를 생성할 수 있다. 이는 동시성 문제를 피하는 데 도움이 된다.

  • 단점
    복잡성 증가: 단순한 설정만 필요한 경우에는 빌더 패턴을 도입하는 것이 오히려 불필요한 복잡성을 초래할 수 있다.

    오버헤드: 빌더 클래스를 정의하고 사용하는 것이 추가적인 코드 오버헤드를 발생시킬 수 있다. 따라서 꼭 필요한 경우에만 사용하는 것이 메모리 관리 측면에서 효율적이다.

    성능: 빌더 패턴은 객체를 생성하기 전에 여러 메서드를 호출해야 하므로, 성능에 민감한 상황에서는 약간의 성능 저하가 발생할 수 있다.

정리

  • ObjectMapper를 사용하여 JSON을 자바 객체로, 또는 반대로 직렬화/역직렬화를 쉽게 할 수 있다.

  • JsonNode란, Jackson 라이브러리에서 제공하는 클래스 중 하나로, JSON 데이터를 트리 구조로 표현하는 데 사용된다. JSON 데이터의 특정 요소에 접근하거나 조작하는 기능을 제공한다.

  • Java의 제네릭(Generics) 타입은 컴파일 시간에만 사용되고, 런타임에는 타입 정보가 사라진다.

  • 커스텀 직렬화/역직렬화기를 만들 수도 있다.

  • ObjectMapperBuilder를 통해 빌더패턴으로 ObjectMapper의 불변 인스턴스를 생성할 수 있다.


참고한 문서: https://www.baeldung.com/jackson-object-mapper-tutorial

profile
화려한 외면이 아닌 단단한 내면

0개의 댓글