여느때와 같이 작업을 하다가 직렬화 에러가 발생해서 관련 개념들을 정리해보려합니다.
직렬화는 자바에서 객체나 데이터를 바이트 스트림으로 변환하여 저장하거나 네트워크를 통해 전송할 수 있도록 만드는 과정입니다. 직렬화된 데이터는 파일, 데이터베이스, 네트워크 등의 외부 환경에서 활용될 수 있습니다.
역직렬화는 직렬화된 데이터를 다시 원래의 객체 형태로 변환하는 과정입니다. 이를 통해 저장된 객체를 다시 활용할 수 있습니다.
객체를 네트워크로 전송하거나 저장할 때, 참조 주소가 아니라 데이터 자체를 전달해야 합니다. 직렬화를 통해 객체를 변환하면, JSON, XML, CSV 등의 형식으로 안전하게 데이터를 주고받을 수 있습니다.
객체 데이터를 올바르게 전송하기 위해서는 메모리 저장 방식을 이해해야 합니다.

값 형식 (Primitive Type): int, long, double, float, boolean 등의 원시 타입은 실제 데이터 값을 메모리에 직접 저장합니다.
참조 형식 (Reference Type): Integer, Long, Double과 같은 래퍼 클래스 또는 사용자가 정의한 객체 타입은 메모리 번지를 통해 참조됩니다.
자바에서 객체를 생성하면 힙(Heap) 메모리에 데이터가 저장되고, 스택(Stack) 메모리에는 해당 객체의 주소값(참조값)이 저장됩니다. 이 주소값을 통해 객체를 참조하는 방식입니다.
객체를 네트워크로 전송할 때, 객체의 참조값(주소)을 그대로 전송하면 문제가 발생합니다.
예를 들어, A 서버가 객체의 데이터를 B 서버로 전송한다고 가정해봅시다.
직렬화를 사용하지 않고 객체를 전송하면 A 서버의 힙 메모리에 있는 참조값이 그대로 전달됩니다.
하지만 B 서버는 A 서버의 힙 메모리에 직접 접근할 수 없기 때문에 해당 객체를 올바르게 사용할 수 없습니다.
이러한 이유로 객체 데이터를 직렬화하여 데이터 형태로 변환한 후 전송해야 합니다.

spring-boot-starter-web을 의존성으로 추가하면 Jackson 라이브러리가 함께 포함됩니다. 이는 스프링이 웹 애플리케이션에서 JSON을 기본 데이터 교환 형식으로 사용하기 때문입니다. Jackson은 가장 널리 사용되는 JSON 직렬화 및 역직렬화 라이브러리로, Spring MVC와 자연스럽게 통합됩니다.
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 문자열을 객체로 역직렬화합니다.
스프링에서는 컨트롤러에서 반환되는 객체를 자동으로 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 문자열로 변환한 후 응답을 생성합니다.
이 과정을 자세히 살펴보면 다음과 같습니다.
@RestController가 적용된 컨트롤러는 기본적으로 JSON 응답을 처리합니다.
ResponseEntity.ok(product)가 반환될 때, 스프링은 HttpMessageConverter를 찾아 적절한 변환기를 선택합니다.
요청 헤더의 Accept: application/json을 확인한 후 Jackson2HttpMessageConverter가 선택됩니다.
Jackson2HttpMessageConverter는 내부적으로 ObjectMapper를 사용하여 객체를 JSON 문자열로 변환합니다.
변환된 JSON이 HTTP 응답 본문에 포함되어 클라이언트로 전송됩니다.
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 어노테이션을 사용하는 이유 중 하나라고 할 수 있을 것 같습니다.