개요
웹 개발을 하다 보면 프론트엔드에서 서버로 데이터를 보내고 받는 일이 일상적입니다.
그런데 우리가 JavaScript 객체로 다루던 데이터가 어떻게 서버까지 전달되고, 다시 객체로 돌아오는 걸까요? 이 과정의 핵심에는 직렬화(Serialization)와 역직렬화(Deserialization)라는 개념이 있습니다.
이번 글에서는 직렬화의 필요성과 어떻게 직렬화가 되는지에 대해서 정리하고자 합니다.
프론트엔드에서의 객체 관리
프론트엔드에서의 객체 관리
프론트엔드에서는 HTTP 요청을 다음과 같이 객체 형태로 관리합니다.
const httpRequestMessage = {
headers: {
'Content-Type': 'application/json',
'Content-Length': 20,
'Accept': '*/*'
},
body: {
userId: "abc",
password: "1234"
}
}
실제 요청을 보낼 때는 axios 같은 라이브러리를 사용해 간단하게 처리합니다.
await axios.post(`${BASE_URL}/auth/signup`, {
userId: "abc",
password: "1234"
});
네트워크를 통한 전송 : 문자열로의 변환
그런데 실제로 서버로 요청이 전송될 때는 다음과 같은 문자열 형태로 변환됩니다.
POST /auth/signup HTTP/1.1
Content-Type: application/json
Content-Length: 20
{"userId": "abc", "password": "1234"}
왜 문자열로 변환해야 할까요❓
네트워크를 통해 데이터를 전송하려면 반드시 연속된 바이트 스트림(문자열)이어야 하기 때문입니다. 객체 형태는 개발자가 조작하기 편하도록 만든 추상화일 뿐, 실제 전송 시에는 문자열로 변환되어야 합니다.
객체를 그대로 보낼 수 없는 이유
1. 메모리 구조의 이해
- RAM은 거대한 배열입니다.
- JavaScript에서 객체를 생성하면 이는 메모리 상에 다음과 같이 흩어져 저장됩니다.
스택: httpRequestMessage → 주소 1000
힙(1000): { headers: 주소 2000, body: 주소 3000 }
힙(2000): { 'Content-Type': 'application/json', ... }
힙(3000): { userId: "abc", password: "1234" }
객체의 실제 데이터는 힙(Heap) 영역 곳곳에 흩어져 있고, 각각은 메모리 주소로 참조됩니다.
주소를 보내면 안 되는 이유
만약 httpRequestMessage의 주소(예: 1000)를 그대로 네트워크를 통해 서버로 보낸다면 어떻게 될까요?
서버는 자신의 메모리 주소 1000번지를 참조하게 되고, 이는 브라우저가 보낸 데이터와는 전혀 다른 데이터입니다. 각 컴퓨터는 독립적인 메모리 공간을 가지고 있기 때문에, 메모리 주소는 해당 프로세스 내에서만 의미가 있습니다.
결론적으로, 메모리 상에 흩어져 있는 데이터를 모두 모아서 하나의 연속된 문자열로 만들어야 합니다.
직렬화 & 역직렬화
직렬화 : 객체를 문자열로
직렬화는 메모리 상에 흩어져 있는 객체를 일렬로 나열된 문자열로 변환하는 과정입니다.
// 객체 (메모리 여기저기 흩어져 있음)
const data = { userId: "abc", password: "1234" };
// 직렬화 (연속된 문자열)
const serialized = JSON.stringify(data);
// '{"userId":"abc","password":"1234"}'
이 문자열은 결국 2진수의 연속체(바이트열)로 변환되어 네트워크를 통해 전송됩니다.
01010000 01001111 01010011 01010100 00100000 00101111 ...
P O S T (space) / ...

위 그림에서 볼 수 있듯이 Binary Encoder를 하면 문자열은 2진수의 연속체인것을 알 수 있습니다.
역직렬화 : 문자열을 다시 객체로
서버에서는 받은 바이트열을 먼저 문자열로 복원하고, 이를 다시 객체로 변환합니다. 이것이 바로 역직렬화입니다.
// 서버에서 받은 문자열
const received = '{"userId":"abc","password":"1234"}';
// 역직렬화 (다시 객체로)
const data = JSON.parse(received);
// { userId: "abc", password: "1234" }
역직렬화를 통해 연속된 바이트열을 다시 메모리의 힙 영역 곳곳에 퍼트려 객체 구조를 복원합니다.
객체로 다루지 않으면 어떻게 될까❓
// 이런 식으로 파싱해야 합니다
const rawString = 'POST /auth/signup HTTP/1.1\nContent-Type: application/json\n\n{"userId":"abc"}';
// HTTP 메서드 추출
const method = rawString.slice(0, rawString.indexOf(' '));
// URL 추출
const urlStart = rawString.indexOf(' ') + 1;
const urlEnd = rawString.indexOf(' ', urlStart);
const url = rawString.slice(urlStart, urlEnd);
// 헤더 파싱
const headerSection = rawString.slice(rawString.indexOf('\n') + 1, rawString.indexOf('\n\n'));
// ... 계속 문자열 조작
이는 C언어에서 네트워크 프로토콜을 다루는 것처럼 매우 번거롭습니다. 반면 객체로 다루면 다음과 같이 간단하게 데이터에 접근 할 수 있습니다.
httpRequestMessage.headers['Content-Type']
- 직렬화
- 메모리 상에 흩어져 있는 객체를 연속된 문자열(바이트열)로 변환하는 과정
- 객체 ➡️ 문자열
- 역직렬화
- 연속된 문자열(바이트열)을 다시 메모리 상의 객체로 복원하는 과정
- 문자열 ➡️ 객체
정리하자면, 네트워크 통신에서 직렬화와 역직렬화가 필요한 이유는 다음과 같습니다.
- 네트워크는 연속된 바이트 스트림만 전송할 수 있습니다
- 메모리 주소는 각 프로세스마다 독립적이어서
그대로 전송할 수 없습니다 - 개발자는 객체 형태로 데이터를 다루는 것이 훨씬 편리합니다
결국 직렬화와 역직렬화는 개발 편의성과 네트워크 전송 요구사항 사이의 완벽한 타협점인 셈입니다.
예시 코드
그렇다면 실제로 클라이언트에서 보낸 데이터가 서버에서 어떻게 받고, 어떻게 직렬화,역직렬화가 되는 지 코드로 살펴보겠습니다.
package com.example.serialization.controller;
import com.example.serialization.dto.SignupRequest;
import com.example.serialization.dto.SignupResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Arrays;
@RestController
@RequestMapping("/auth")
public class AuthController {
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* [POST] 직렬화(Serialization) 과정 확인
* 클라이언트가 보낸 데이터가 어떻게 이진수 연속체로 변하는지 보여줍니다.
*/
@PostMapping("/signup")
public void showSerialization(@RequestBody SignupRequest request,
@RequestHeader(value = "Content-Type") String contentType) throws Exception {
String jsonBody = objectMapper.writeValueAsString(request);
String rawHttpRequest = "POST /auth/signup HTTP/1.1\n" +
"Content-Type : " + contentType + "\n\n" +
jsonBody;
byte[] bytes = rawHttpRequest.getBytes(StandardCharsets.UTF_8);
StringBuilder binaryStream = new StringBuilder();
for (byte b : bytes) {
binaryStream.append(String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'));
}
System.out.println("\n[클라이언트에서 보낸 데이터 (Raw 문자열)]");
System.out.println(rawHttpRequest);
System.out.println("\n[서버가 수신한 데이터 (직렬화된 이진수 연속체)]");
System.out.println(binaryStream.toString());
System.out.println( );
}
/**
* [GET] 역직렬화(Deserialization) 과정 확인
* 수신한 문자열이 어떻게 다시 Java 객체로 복원되는지 보여줍니다.
*/
@GetMapping("/user-info")
public void showDeserialization() throws Exception {
// 서버가 수신한 데이터라고 가정 (Raw 문자열)
String receivedRaw = "{\"userId\":\"testUser\",\"password\":\"password123\"}";
// 역직렬화 진행 (문자열 -> 객체)
SignupRequest restoredObj = objectMapper.readValue(receivedRaw, SignupRequest.class);
System.out.println("\n[서버가 수신한 데이터 (Raw 문자열)]");
System.out.println(receivedRaw);
System.out.println("\n[역직렬화된 후 객체 (Java Object)]");
System.out.println("객체 정보: " + restoredObj);
}
}
1. 데이터의 출발: 클라이언트에서의 직렬화
클라이언트가 객체를 전송하면, 네트워크를 타고 가기 위해 데이터는 이진수(Binary)의 형태로 바뀝니다.

위 로그를 보면 알 수 있듯이, 클라이언트에서 보낸 객체는 서버에 도착할 때 우리가 읽을 수 없는 바이트(이진수) 문자열로 보내진 것을 확인할 수 있습니다.
컴퓨터는 오직 이 '0'과 '1'의 조합만을 이해하며 데이터를 주고받습니다.
2. 텍스트로의 변환 (JSON)
01010000010011110101001101010100001000000010111101100001011101010111010001101000001011110111001101101001011001110110111001110101011100000010000001001000010101000101010001010000001011110011000100101110001100010000101001000011011011110110111001110100011001010110111001110100001011010101010001111001011100000110010100100000001110100010000001100001011100000111000001101100011010010110001101100001011101000110100101101111011011100010111101101010011100110110111101101110000010100000101001111011001000100111010101110011011001010111001001001001011001000010001000111010001000100111010001100101011100110111010001010101011100110110010101110010001000100010110000100010011100000110000101110011011100110111011101101111011100100110010000100010001110100010001001110000011000010111001101110011011101110110111101110010011001000011000100110010001100110010001001111101
실제 바이트 문자열은 위와 같습니다.

서버는 수신한 이진 데이터를 설정된 인코딩(UTF-8)에 맞춰 우리가 읽을 수 있는 텍스트로 해석합니다. 이것이 바로 HTTP Body에 담긴 JSON 문자열입니다.

바이트 데이터를 문자열로 변환하니, 이제야 비로소 우리가 의도했던 데이터의 형태가 보이기 시작합니다. 하지만 아직은 단순한 '글자'일 뿐, 자바가 다룰 수 있는 '객체'는 아닙니다.
3. 서버에서의 역직렬화 (Java Object)
이제 Spring의 ObjectMapper가 이 JSON 문자열을 분석하여 실제 자바 클래스인 SignupRequest 객체에 값을 채워넣습니다. 이 과정을 역직렬화(Deserialization)라고 합니다.

보시다시피 최종적으로는 클라이언트가 보낸 데이터가 자바 객체 형태로 복원된 것을 알 수 있습니다. 이제 개발자는 request.getUserId()와 같은 메서드를 통해 데이터를 자유롭게 사용할 수 있게 됩니다.
Spring Boot는 내부적으로 Jackson이라는 강력한 라이브러리를 사용하여 이 복잡한 과정을 처리합니다. 덕분에 우리는 바이트 단위의 데이터를 고민할 필요 없이 @RequestBody 어노테이션 하나로 편리하게 객체를 받아볼 수 있는 것입니다.
'Framework > Spring\Spring boot' 카테고리의 다른 글
| [Spring boot] 동기식 HTTP 클라이언트 RestTemplate 알아보기 (0) | 2026.01.07 |
|---|---|
| [Springboot] Spring Boot에서 Service와 ServiceImpl 분리, 꼭 필요할까❓ (0) | 2025.02.28 |
| [SpringBoot] ObjectMapper와 직렬화/역직렬화 (0) | 2025.02.27 |
| [Spring boot] DTO와 Entity 변환 위치에 대한 고찰 - Controller vs Service Layer (0) | 2024.12.25 |
| [Spring boot] Cache Manager와 @Cacheable 어노테이션 이해하기 (0) | 2024.09.02 |