직렬화와 역직렬화란? 자바 개발자가 꼭 알아야 할 개념 정리


데이터를 저장하거나 네트워크로 전송할 때, 직렬화와 역직렬화라는 단어를 반드시 마주치게 됩니다. 처음 들으면 낯설지만, 사실 우리가 매일 사용하는 로그인 세션 유지·API 통신·파일 저장 모두 이 원리 위에서 작동합니다. 이 글에서는 개념의 본질부터 자바 코드 예제, 보안 주의사항, 실전 포맷 선택 기준까지 한 번에 정리합니다.


목차

  1. 직렬화와 역직렬화란 무엇인가?
  2. 자바 직렬화의 핵심 원리와 동작 메커니즘
  3. 직렬화의 긍정적 효과와 주요 활용 사례
  4. 역직렬화 취약점과 반드시 피해야 할 함정
  5. 실전 단계별 활용법 — 자바부터 JSON·Protobuf까지
  6. 전문가·기관 관점 및 직렬화 포맷 선택 가이드

1. 직렬화와 역직렬화란 무엇인가?

프로그래밍에서 **직렬화(Serialization)**란 메모리에 살아 있는 객체(Object)를, 파일에 저장하거나 네트워크로 전송할 수 있도록 연속된 바이트(byte) 스트림이나 텍스트 형식으로 변환하는 과정입니다. 반대로 **역직렬화(Deserialization)**는 그 변환된 데이터를 다시 원래의 객체로 복원하는 과정입니다.

일상 속 직렬화 비유

택배를 생각해 보세요. 유리 꽃병(객체)을 그대로 트럭에 실을 수는 없습니다. 신문지로 감싸고, 박스에 넣고, 완충재를 채우는 과정(직렬화)을 거쳐야 안전하게 운송할 수 있습니다. 목적지에 도착하면 박스를 열고 꽃병을 꺼내 원래 형태로 복원(역직렬화)합니다. 직렬화는 정확히 이 과정을 소프트웨어 세계에서 구현합니다.

왜 필요한가?

컴퓨터 메모리에 있는 객체는 프로그램이 종료되면 사라집니다. 또한 서로 다른 머신이나 프로세스 사이에서는 메모리 주소가 의미를 갖지 않습니다. 직렬화는 이 두 가지 한계를 해결합니다.

  • 영속성(Persistence): 객체를 파일·DB에 저장해 프로그램 재시작 후에도 상태 복원
  • 통신(Communication): 네트워크를 통해 다른 시스템으로 객체를 전달
  • 깊은 복사(Deep Copy): 객체를 직렬화 후 역직렬화하면 완전히 독립된 복사본 생성

직렬화 형식에는 자바 기본 바이너리, JSON, XML, YAML, Protocol Buffers, MessagePack 등 다양한 포맷이 존재하며, 상황에 따라 선택이 달라집니다.

[객체 (메모리)] ──직렬화──▶ [바이트 스트림 / JSON / XML] ──역직렬화──▶ [객체 복원]

2. 자바 직렬화의 핵심 원리와 동작 메커니즘

자바는 java.io.Serializable 인터페이스를 통해 내장 직렬화 메커니즘을 제공합니다. 이 인터페이스는 **메서드가 하나도 없는 마커 인터페이스(Marker Interface)**로, JVM에게 “이 객체는 직렬화 가능하다”는 신호를 보내는 역할만 합니다.

Serializable과 serialVersionUID

java

import java.io.Serializable;

public class Member implements Serializable {

    // 직렬화 버전 관리용 고유 ID
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    private transient String password; // transient: 직렬화 제외

    public Member(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }

    @Override
    public String toString() {
        return "Member{name='" + name + "', age=" + age + ", password='" + password + "'}";
    }
}

serialVersionUID 는 클래스 버전을 식별하는 고유 번호입니다. 직렬화된 데이터를 역직렬화할 때, 현재 클래스의 UID와 저장된 UID가 다르면 InvalidClassException이 발생합니다. 명시적으로 선언하지 않으면 JVM이 자동 생성하지만, 클래스를 조금만 수정해도 UID가 바뀌어 기존 데이터를 읽지 못하는 문제가 생깁니다. 반드시 직접 선언하는 것이 원칙입니다.

transient 키워드가 붙은 필드는 직렬화에서 제외됩니다. 비밀번호·카드 번호처럼 민감한 정보나, DB 연결·스레드처럼 직렬화가 불가능한 객체에 활용합니다.

ObjectOutputStream / ObjectInputStream 실전 코드

java

import java.io.*;

public class SerializationDemo {

    // 직렬화: 객체 → 파일
    public static void serialize(Member member, String filePath) throws IOException {
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(filePath))) {
            oos.writeObject(member);
            System.out.println("직렬화 완료: " + filePath);
        }
    }

    // 역직렬화: 파일 → 객체
    public static Member deserialize(String filePath)
            throws IOException, ClassNotFoundException {
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(filePath))) {
            Member member = (Member) ois.readObject();
            System.out.println("역직렬화 완료: " + member);
            return member;
        }
    }

    public static void main(String[] args) throws Exception {
        Member member = new Member("홍길동", 30, "secret123");
        String path = "member.ser";

        serialize(member, path);
        Member restored = deserialize(path);

        // transient 필드는 null로 복원됨
        // 출력: Member{name='홍길동', age=30, password='null'}
    }
}

내부적으로 JVM은 리플렉션(Reflection)을 사용해 객체의 필드를 탐색하고 바이트 스트림으로 인코딩합니다. 상속 계층도 함께 직렬화되며, 부모 클래스도 Serializable을 구현해야 정상 동작합니다. 부모가 구현하지 않은 경우, 부모 클래스의 기본 생성자가 호출되어 필드는 기본값으로 초기화됩니다.


3. 직렬화의 긍정적 효과와 주요 활용 사례

직렬화가 실제 서비스에서 어떻게 쓰이는지 구체적으로 살펴보겠습니다.

세션(Session) 클러스터링

대규모 웹 서비스는 여러 대의 서버(WAS)를 운용합니다. 사용자의 로그인 세션 객체를 Redis나 Memcached 같은 외부 저장소에 저장하려면 반드시 직렬화가 필요합니다. Spring Session은 내부적으로 직렬화를 사용해 세션 객체를 바이트 배열로 변환 후 Redis에 저장하고, 다른 서버에서 역직렬화하여 동일한 세션을 제공합니다.

메시지 큐(Message Queue)와 이벤트 스트리밍

Apache Kafka, RabbitMQ와 같은 메시지 브로커는 메시지를 바이트 배열로 전송합니다. 이때 자바 객체를 JSON 또는 Avro/Protobuf로 직렬화하여 프로듀서가 전송하고, 컨슈머가 역직렬화하여 사용합니다. 마이크로서비스 아키텍처에서 서비스 간 데이터 교환의 핵심 매커니즘입니다.

캐시(Cache) 저장

복잡한 연산 결과나 DB 조회 결과를 캐시에 저장할 때도 직렬화가 사용됩니다. 예를 들어 Spring Cache + Redis 조합에서는 메서드 반환 객체를 자동으로 직렬화하여 캐시에 저장하고, 캐시 히트 시 역직렬화하여 반환합니다.

JPA/Hibernate와 분산 환경

JPA 엔티티를 2차 캐시(Ehcache, Hazelcast)에 저장하거나, Java RMI(Remote Method Invocation)로 원격 메서드를 호출할 때도 직렬화가 필수입니다. RMI는 자바 네이티브 직렬화를 사용해 메서드 파라미터와 반환값을 네트워크로 전달합니다.

깊은 복사(Deep Copy) 구현

객체를 직렬화했다가 즉시 역직렬화하면, 내부 참조 객체까지 모두 복사된 완전히 독립적인 복사본을 얻을 수 있습니다. 복잡한 객체 그래프의 깊은 복사를 간단하게 구현하는 방법으로 활용됩니다(단, 성능 비용 고려 필요).


4. 역직렬화 취약점과 반드시 피해야 할 함정

직렬화는 강력하지만, 잘못 사용하면 심각한 보안 취약점과 유지보수 문제를 야기합니다. 이 섹션은 반드시 숙지해야 합니다.

역직렬화 보안 취약점 (CVE 수준 위협)

자바 기본 직렬화의 가장 큰 위험은 신뢰할 수 없는 소스에서 온 데이터를 역직렬화할 때 발생합니다. 공격자가 악의적으로 조작된 직렬화 데이터를 서버로 전송하면, readObject() 호출 시 임의 코드가 실행될 수 있습니다.

대표적 사례:

  • Apache Commons Collections 취약점 (2015): 역직렬화 체인을 통한 원격 코드 실행(RCE). WebLogic, JBoss, Jenkins 등 주요 자바 서버들이 영향받음.
  • Spring 프레임워크 역직렬화 취약점: 특정 버전에서 조작된 객체 스트림으로 RCE 가능.

대응 방법:

java

// 1. ObjectInputFilter 사용 (Java 9+) — 허용 클래스 화이트리스트
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(filterInfo -> {
    Class<?> clazz = filterInfo.serialClass();
    if (clazz == null) return ObjectInputFilter.Status.UNDECIDED;
    // Member 클래스만 허용
    return clazz == Member.class
        ? ObjectInputFilter.Status.ALLOWED
        : ObjectInputFilter.Status.REJECTED;
});

java

// 2. 신뢰할 수 없는 입력은 절대 역직렬화 금지
// 네트워크로 받은 바이트를 직접 readObject()에 넣는 코드는 위험

serialVersionUID 누락과 클래스 변경 문제

serialVersionUID를 명시하지 않은 상태에서 클래스에 필드 하나만 추가해도 자동 생성 UID가 달라져, 기존에 저장된 모든 직렬화 데이터를 읽을 수 없게 됩니다. 장기 저장 데이터를 다루는 시스템에서는 치명적인 운영 장애로 이어집니다.

transient 누락과 민감 데이터 유출

비밀번호, 개인 식별 정보, 카드 번호 등에 transient를 붙이지 않으면, 직렬화 파일에 그대로 저장되어 유출 위험이 생깁니다. 직렬화 파일은 단순 텍스트 에디터로도 부분적으로 읽을 수 있습니다.

자바 기본 직렬화의 성능과 호환성 한계

자바 기본 직렬화 포맷은 자바 전용이라 다른 언어와 호환되지 않으며, JSON 대비 바이트 크기가 크고 역직렬화 속도도 상대적으로 느립니다. 이펙티브 자바(Effective Java) 3판의 저자 조슈아 블로크(Joshua Bloch)는 **”자바 직렬화는 설계 실수였으며, 가능하면 사용을 피하라”**고 권고합니다.


5. 실전 단계별 활용법 — 자바부터 JSON·Protobuf까지

현대 자바 개발에서는 자바 기본 직렬화보다 JSON, Protocol Buffers, MessagePack 같은 대안 포맷을 주로 사용합니다.

Jackson을 사용한 JSON 직렬화/역직렬화

Spring Boot의 기본 JSON 라이브러리인 Jackson은 가장 널리 쓰이는 직렬화 도구입니다.

java

import com.fasterxml.jackson.databind.ObjectMapper;

public class JacksonDemo {

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();

        // 직렬화: 객체 → JSON 문자열
        Member member = new Member("이순신", 45, "secret");
        String json = mapper.writeValueAsString(member);
        System.out.println("JSON: " + json);
        // {"name":"이순신","age":45}  (password는 @JsonIgnore로 제외 가능)

        // 역직렬화: JSON 문자열 → 객체
        Member restored = mapper.readValue(json, Member.class);
        System.out.println("복원: " + restored.getName());

        // 파일로 저장
        mapper.writeValue(new File("member.json"), member);

        // 파일에서 읽기
        Member fromFile = mapper.readValue(new File("member.json"), Member.class);
    }
}

유용한 Jackson 어노테이션:

어노테이션역할
@JsonIgnore직렬화/역직렬화에서 해당 필드 제외
@JsonProperty("name")JSON 키 이름 커스터마이징
@JsonInclude(NON_NULL)null 필드 직렬화 제외
@JsonFormat날짜·숫자 포맷 지정
@JsonTypeInfo다형성 타입 정보 포함

Protocol Buffers (Protobuf) — 고성능 바이너리 직렬화

Google이 개발한 Protobuf는 JSON 대비 3~10배 빠른 직렬화 속도와 훨씬 작은 데이터 크기를 제공합니다. 마이크로서비스 간 gRPC 통신에 표준으로 사용됩니다.

protobuf

// member.proto
syntax = "proto3";

message Member {
    string name = 1;
    int32 age = 2;
}

java

// 자바 코드에서 사용
Member member = Member.newBuilder()
    .setName("강감찬")
    .setAge(55)
    .build();

// 직렬화
byte[] bytes = member.toByteArray();

// 역직렬화
Member restored = Member.parseFrom(bytes);

포맷별 선택 기준 요약

상황                          권장 포맷
─────────────────────────────────────────────
REST API 응답/요청           Jackson JSON
마이크로서비스 내부 통신     Protocol Buffers / Avro
Redis 캐시                   Jackson JSON (가독성) or Kryo (성능)
Kafka 메시지                 Avro (Schema Registry 연동 시) or JSON
파일 설정 저장               Jackson JSON / SnakeYAML
자바 객체 깊은 복사          BeanUtils / ModelMapper (직렬화 비권장)

직렬화 단계별 적용 체크리스트

  1. Serializable 구현 여부 결정: 자바 기본 직렬화가 꼭 필요한가? 대안 포맷은 없는가?
  2. serialVersionUID 명시: 반드시 직접 선언.
  3. transient 적용: 민감 정보 및 직렬화 불필요 필드 지정.
  4. 입력 신뢰 여부 확인: 외부 입력은 절대 역직렬화 전 검증.
  5. 포맷 선택: 성능·호환성·가독성 기준으로 JSON/Protobuf/Avro 중 선택.
  6. 테스트: 직렬화 → 역직렬화 후 객체 동등성(equals) 검증.

6. 전문가·기관 관점 및 직렬화 포맷 선택 가이드

이펙티브 자바(Effective Java)의 권고

조슈아 블로크는 Effective Java 3판 아이템 85~90에서 자바 직렬화에 대해 다음과 같이 명확히 권고합니다.

  • 아이템 85: “자바 직렬화를 피할 수 없다면, 신뢰할 수 없는 데이터는 절대 역직렬화하지 마라.”
  • 아이템 86: “Serializable을 구현할지 신중히 결정하라. 한 번 공개되면 영원히 지원해야 한다.”
  • 아이템 87: “커스텀 직렬화 형식 사용을 고려하라.”
  • 아이템 90: “직렬화된 인스턴스 대신 직렬화 프록시 패턴을 사용하라.”

이 권고들의 핵심은 **”자바 기본 직렬화는 유지보수 비용이 높고 보안 위험이 크므로, JSON 등 대안을 우선 검토하라”**는 것입니다.

OWASP의 역직렬화 보안 가이드

OWASP(Open Web Application Security Project)는 **안전하지 않은 역직렬화(Insecure Deserialization)**를 OWASP Top 10 웹 애플리케이션 보안 위협 목록에 포함시킨 바 있습니다. 권고 사항은 다음과 같습니다.

  • 신뢰할 수 없는 소스의 데이터는 역직렬화하지 않는다.
  • 역직렬화 전 디지털 서명이나 HMAC으로 무결성 검증을 수행한다.
  • Java 9+의 ObjectInputFilter로 허용 클래스를 화이트리스트 방식으로 제한한다.
  • 역직렬화 과정을 별도의 낮은 권한 환경에서 실행한다.

Spring Framework와 Jackson의 모범 사례

Spring Boot는 기본적으로 Jackson ObjectMapper를 통해 HTTP 요청/응답의 직렬화를 처리합니다. Spring Security와 함께 사용할 때는 HttpSessionSecurityContextRepository가 내부적으로 자바 직렬화를 사용하므로, Spring Session으로 교체하고 JSON 직렬화로 전환하는 것이 권장됩니다.

추천 도구 및 라이브러리

목적라이브러리특징
JSONJacksonSpring Boot 기본, 가장 범용적
JSONGsonGoogle 제공, 경량, 레거시 코드
JSONMoshiKotlin 친화적, 코루틴 지원
고성능 바이너리Protocol BuffersGoogle 개발, gRPC 표준
고성능 바이너리Apache AvroKafka Schema Registry 연동
고성능 자바 전용KryoRedis 캐시 등 자바 내부 한정 사용

결론

직렬화와 역직렬화는 객체를 저장·전송 가능한 형태로 바꾸고 다시 원래 상태로 복원하는, 현대 소프트웨어의 필수 기반 기술입니다. 자바의 Serializable은 이해를 위한 출발점이지만, 실무에서는 보안·성능·호환성을 고려해 Jackson JSON이나 Protocol Buffers를 우선 선택하는 것이 정석입니다. 오늘 배운 내용을 바탕으로 자신의 프로젝트에서 사용 중인 직렬화 방식을 한 번 점검해 보세요.


⚠️ 면책 고지: 본 포스트에 포함된 코드 예제와 보안 권고사항은 교육 목적으로 작성되었습니다. 실제 프로덕션 환경에 적용 시에는 해당 프레임워크·라이브러리의 최신 공식 문서와 보안 업데이트를 반드시 확인하고, 전문가 검토를 거치시기 바랍니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다