[Spring] 직렬화(Serialization)와 역직렬화(Deserialization)의 개념 및 활용

김성재·2025년 2월 16일

여느때와 같이 작업을 하다가 직렬화 에러가 발생해서 관련 개념들을 정리해보려합니다.

1️⃣직렬화와 역직렬화란?

🔹직렬화(Serialization)란?

직렬화는 자바에서 객체나 데이터를 바이트 스트림으로 변환하여 저장하거나 네트워크를 통해 전송할 수 있도록 만드는 과정입니다. 직렬화된 데이터는 파일, 데이터베이스, 네트워크 등의 외부 환경에서 활용될 수 있습니다.

🔹역직렬화(Deserialization)란?

역직렬화는 직렬화된 데이터를 다시 원래의 객체 형태로 변환하는 과정입니다. 이를 통해 저장된 객체를 다시 활용할 수 있습니다.

🔹객체를 직렬화해야 하는 이유

객체를 네트워크로 전송하거나 저장할 때, 참조 주소가 아니라 데이터 자체를 전달해야 합니다. 직렬화를 통해 객체를 변환하면, JSON, XML, CSV 등의 형식으로 안전하게 데이터를 주고받을 수 있습니다.

2️⃣ 왜 객체 형태는 그대로 전송할 수 없을까?

🔹 메모리 저장 구조 이해

객체 데이터를 올바르게 전송하기 위해서는 메모리 저장 방식을 이해해야 합니다.

  • 값 형식 (Primitive Type): int, long, double, float, boolean 등의 원시 타입은 실제 데이터 값을 메모리에 직접 저장합니다.

  • 참조 형식 (Reference Type): Integer, Long, Double과 같은 래퍼 클래스 또는 사용자가 정의한 객체 타입은 메모리 번지를 통해 참조됩니다.

자바에서 객체를 생성하면 힙(Heap) 메모리에 데이터가 저장되고, 스택(Stack) 메모리에는 해당 객체의 주소값(참조값)이 저장됩니다. 이 주소값을 통해 객체를 참조하는 방식입니다.

🔹 네트워크 전송 시 발생하는 문제

객체를 네트워크로 전송할 때, 객체의 참조값(주소)을 그대로 전송하면 문제가 발생합니다.

예를 들어, A 서버가 객체의 데이터를 B 서버로 전송한다고 가정해봅시다.

  • 직렬화를 사용하지 않고 객체를 전송하면 A 서버의 힙 메모리에 있는 참조값이 그대로 전달됩니다.

  • 하지만 B 서버는 A 서버의 힙 메모리에 직접 접근할 수 없기 때문에 해당 객체를 올바르게 사용할 수 없습니다.

이러한 이유로 객체 데이터를 직렬화하여 데이터 형태로 변환한 후 전송해야 합니다.

3️⃣ 스프링에서 JSON 변환이 자동으로 이루어지는 이유

📌 spring-boot-starter-web 의존성을 추가하면 Jackson 라이브러리가 포함되는 이유


spring-boot-starter-web을 의존성으로 추가하면 Jackson 라이브러리가 함께 포함됩니다. 이는 스프링이 웹 애플리케이션에서 JSON을 기본 데이터 교환 형식으로 사용하기 때문입니다. Jackson은 가장 널리 사용되는 JSON 직렬화 및 역직렬화 라이브러리로, Spring MVC와 자연스럽게 통합됩니다.

📌 ObjectMapper를 활용한 JSON 변환

Jackson의 핵심 클래스 중 하나인 ObjectMapper는 자바 객체를 JSON으로 변환하거나 JSON을 자바 객체로 변환하는 역할을 합니다.

import com.fasterxml.jackson.databind.ObjectMapper;

public class JacksonExample {
    public static void main(String[] args) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();
        Product product = new Product("Smartphone", 899.99, 20);
        
        // 객체를 JSON 문자열로 변환
        String jsonString = objectMapper.writeValueAsString(product);
        System.out.println("JSON Output: " + jsonString);
        
        // JSON 문자열을 객체로 변환
        Product deserializedProduct = objectMapper.readValue(jsonString, Product.class);
        System.out.println("Deserialized Product: " + deserializedProduct.getProductName());
    }
}

위 코드에서 ObjectMapper는 writeValueAsString()을 통해 객체를 JSON 문자열로 변환하고, readValue()를 통해 JSON 문자열을 객체로 역직렬화합니다.

📌 Jackson2HttpMessageConverter의 역할

스프링에서는 컨트롤러에서 반환되는 객체를 자동으로 JSON으로 변환합니다. 이 과정에서 Jackson2HttpMessageConverter가 ObjectMapper를 사용하여 변환 작업을 수행합니다.

@RestController
@RequestMapping("/products")
public class ProductController {

    @GetMapping("/{id}")
    public ResponseEntity<ProductResponse> getProduct(@PathVariable Long id) {
        ProductResponse product = new ProductResponse(id, "Tablet", 499.99, 5);
        return ResponseEntity.ok(product);
    }
}

위와 같은 API 요청이 들어오면 ProductResponse 객체가 반환됩니다. 이때 스프링은 내부적으로 Jackson2HttpMessageConverter를 활용하여 객체를 JSON 문자열로 변환한 후 응답을 생성합니다.

이 과정을 자세히 살펴보면 다음과 같습니다.

  1. @RestController가 적용된 컨트롤러는 기본적으로 JSON 응답을 처리합니다.

  2. ResponseEntity.ok(product)가 반환될 때, 스프링은 HttpMessageConverter를 찾아 적절한 변환기를 선택합니다.

  3. 요청 헤더의 Accept: application/json을 확인한 후 Jackson2HttpMessageConverter가 선택됩니다.

  4. Jackson2HttpMessageConverter는 내부적으로 ObjectMapper를 사용하여 객체를 JSON 문자열로 변환합니다.

  5. 변환된 JSON이 HTTP 응답 본문에 포함되어 클라이언트로 전송됩니다.

5️⃣ JSON 변환을 위해 필요한 조건

✅ 직렬화를 위해 Getter 메서드를 추가하자

Jackson은 기본적으로 객체의 Getter 메서드를 사용하여 JSON 데이터를 생성합니다. 만약 Getter가 정의되지 않은 필드가 있다면 해당 필드는 JSON 변환 과정에서 제외됩니다.

public class ProductResponse {
    private final String name;
    private final double price;

    public ProductResponse(String name, double price) {
        this.name = name;
        this.price = price;
    }
}

위와 같은 코드에서 getName()과 getPrice() 메서드가 없으면 JSON 변환이 실패하거나 필드가 누락될 수 있습니다. 따라서 반드시 Getter 메서드를 정의해야 합니다.

✅ 역직렬화를 위해 기본 생성자를 추가하자

Jackson이 JSON 데이터를 객체로 변환(역직렬화)할 때 기본 생성자를 사용하여 객체를 생성합니다. 따라서 기본 생성자가 없으면 역직렬화가 실패할 수 있습니다.

public class ProductRequest {
    private String name;
    private double price;

    public ProductRequest() {} // 기본 생성자 필수

    public ProductRequest(String name, double price) {
        this.name = name;
        this.price = price;
    }
}

기본 생성자가 없으면 Jackson이 객체를 생성할 수 없기 때문에 400 Bad Request 또는 500 Internal Server Error가 발생할 수 있습니다. 스프링에서 @NoArgsConstructor 어노테이션을 사용하는 이유 중 하나라고 할 수 있을 것 같습니다.

참고자료

0개의 댓글