Serialization(직렬화)

자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트(Byte) 형태로 변환하는 기술이다.

핵심 구성 요소

ObjectOutputStream & ObjectInputStream

java.io 패키지의 각각 객체의 직렬화와 역직렬화를 처리하는 스트림 클래스로, 기본적인 직렬화/역직렬화 메커니즘을 제공한다.

  • ObjectOutputStream: 객체를 직렬화하여 출력 스트림에 기록하는 역할

    • writeObject(Object obj) 메서드를 통해 객체를 직렬화

    • 직렬화 시 객체의 메타데이터와 데이터 필드들을 모두 바이트 스트림으로 변환

  • ObjectInputStream: 스트림으로부터 바이트를 읽어 객체로 복원하는 역할

    • readObject() 메서드를 통해 역직렬화 수행

    • 역직렬화 시 직렬화된 클래스의 바이트코드가 CLASSPATH에 존재해야 함

Serializable 인터페이스

직렬화를 허용할 클래스는 반드시 java.io.Serializable 인터페이스를 구현해야 한다.

  • 마커 인터페이스(Marker Interface)

    • 구현해야 할 메서드가 없는 빈 인터페이스

    • JVM에게 해당 객체는 직렬화가 가능하다는 것을 알리는 역할

  • 구현하지 않은 경우

    • 직렬화 시도 시 java.io.NotSerializableException 예외 발생

transient 키워드

transient 키워드는 직렬화 과정에서 특정 필드를 제외하고 싶을 때 사용된다.

  • 패스워드 등 보안상 민감한 데이터나 스트림으로 전송할 필요가 없는 임시 데이터 제외

  • 역직렬화 결과: 해당 필드는 null이나 0과 같은 기본값(Default Value)으로 복원

serialVersionUID

직렬화된 객체와 역직렬화할 클래스의 버전 일치 여부를 확인하기 위한 고유 식별자(ID)이다.

  • 동작 원리

    • 명시하지 않으면 컴파일러가 클래스 구조를 기반으로 해시값을 생성하여 자동 할당

    • 클래스에 필드가 추가되거나 변경되면 이 ID 값이 변하게 됨

  • 실무 권장사항

    • 클래스 변경(필드 추가/삭제) 시 역직렬화 실패(InvalidClassException)를 방지하기 위해 반드시 static final long으로 명시할 것을 권장함

    • 예: private static final long serialVersionUID = 1L;

커스텀 직렬화 메서드

Serializable 인터페이스를 구현한 클래스 내부에 특정 메서드를 정의하면, JVM이 직렬화 프로세스 중에 이를 자동으로 호출하여 로직을 가로챌 수 있게 해준다.

메서드
시점
역할 및 특징

writeReplace

직렬화 직전

본래의 객체 대신 다른 객체를 직렬화하도록 교체 가능 (프록시 패턴 구현 시 사용)

writeObject

직렬화 중

스트림에 데이터를 쓰는 방식을 커스터마이징 (암호화 등 수행)

readObject

역직렬화 중

스트림에서 데이터를 읽는 방식을 커스터마이징 (복호화, 유효성 검사 등 수행)

readResolve

역직렬화 직후

역직렬화된 객체를 다른 객체로 대체 (싱글톤 보장 시 필수 사용)

Serializable 인터페이스 구현 예시

아래는 Serializable 인터페이스를 구현한 예시 코드로, 수행되는 순서는 다음과 같다.

  1. writeReplace: 직렬화 전, 직렬화될 인스턴스를 대체하거나 변경

  2. writeObject: 직렬화 과정 중 객체 데이터를 직접 쓰는 방법을 정의

  3. readObject: 역직렬화 과정 중 객체 데이터를 직접 읽는 방법을 정의

  4. readResolve: 역직렬화 후, 반환할 인스턴스를 대체하거나 변경

실무적 고려사항과 대안

자바 직렬화는 편리하지만 실무에서는 다음과 같은 이유로 사용을 지양하거나 주의해서 사용한다.

  • 보안 이슈

    • 역직렬화 과정에서 공격자가 조작한 바이트 스트림을 보내 원격 코드 실행(RCE) 등의 심각한 보안 취약점을 유발할 수 있음

  • 유지보수성

    • 클래스 구조가 변경되면 역직렬화가 불가능해지는 등 장기적인 데이터 저장 포맷으로 적합하지 않음

  • 대안 기술 사용 권장

    • 데이터 교환 목적: JSON (Jackson, Gson), XML

    • 성능 목적: Protocol Buffers, Avro 등 이식성과 성능이 뛰어난 포맷 사용

싱글톤 패턴과 readResolve

일반적으로 싱글톤(Singleton) 패턴은 애플리케이션 내에서 단 하나의 인스턴스만 존재함을 보장해야 하지만, 직렬화와 역직렬화를 거치면 이 원칙이 깨질 수 있다.

  • 기본적으로 역직렬화(ObjectInputStream.readObject)는 생성자를 호출하지 않고 힙 메모리에 새로운 객체를 생성하는 방식으로 동작

  • 싱글톤 객체를 직렬화한 후 다시 역직렬화하면, 기존의 싱글톤 인스턴스와는 별개의 새로운 인스턴스가 생성

이를 방지하기 위해 readResolve 메서드를 사용하여 역직렬화된 객체 대신 기존에 생성된 싱글톤 인스턴스를 반환하도록 구현해야 한다.

이 외에도 Enum 타입 사용을 통해 싱글톤을 구현하는 방법도 직렬화 문제를 자연스럽게 해결할 수 있는 대안으로 권장된다.

Last updated

Was this helpful?