직렬화
직렬화는 자바 객체의 상태를 바이트 스트림(Byte Stream)으로 변환하는 과정이다.
- 목적: 객체를 파일에 저장하거나 네트워크를 통해 전송하기 위함으로, 객체는 메모리에 존재하지만 파일 저장이나 네트워크 전송을 위해 연속적인 바이트 형태로 변환되어야 한다.
- 방법: 직렬화하려는 클래스는 반드시 java.io.Serializable 인터페이스를 구현해야 한다. 이 인터페이스는 마커 인터페이스로 아무런 메소드를 미포함하지만 해당 클래스의 객체가 직렬화될 수 있음을 JVM에 알려주는 역할을 한다.
- 주체: java.io.ObjectOutputStream 클래스의 wirteObject() 메소드를 사용하여 수행한다.
역직렬화
역직렬화는 바이트 스트림으로 변환된 객체 데이터를 다시 본래의 자바 객체로 복원하는 과정이다.
-
목적: 파일에 저장되거나 네트워크로 전송된 바이트 스트림을 읽어 메모리 상의 객체로 재구성하기 위함이다.
-
방법: 직렬화된 객체를 복원할 때, 클래스 로더는 해당 클래스를 찾고 객체를 생성한다.
-
주체: java.io.ObjectInputStream 클래스의 readObject() 메소드를 사용하여 수행되며, 반환되는 Object 타입을 원래의 클래스 타입으로 캐스팅해야 한다.
serialVersionUID
직렬화 가능한 클래스는 private static final long serialVersionUID 필드를 선언하는 것을 권장한다.
-
역할: 직렬화된 객체와 이를 역직렬화할 클래스 간의 버전 호환성을 확인하는 고유 식별자이다.
-
작동 방식: 객체를 직렬화할 때 이 ID가 기록되고, 역직렬화 시 복원하려는 클래스의 ID와 비교한다. 두 ID가 일치하지 않으면 InvalidClassException 예외가 발생하여 역직렬화에 실패한다.
-
미선언 시: 개발자가 명시하지 않으면 자바 컴파일러가 클래스의 구조를 기반으로 자동으로 생성하지만, 클래스 구조가 조금만 바뀌어도 값이 변해버려 호환성 문제가 생길 수 있다. 따라서 명시적으로 선언하는 것이 안전하다.
직렬화의 내부 원리 및 작동 방식
직렬화는 객체의 단순한 복사가 아닌, 객체의 구조와 상태를 정밀하게 기록하는 과정이다.
-
객체 그래프(Object Graph) 직렬화: 직렬화는 단순히 해당 객체만 저장하는 것이 아니라, 그 객체가 참조하고 있는 모든 객체를 재귀적으로 따라가면서 함께 직렬화한다. 만약 한 객체가 동일한 다른 객체를 여러 번 참고한다면, 직렬화 매커니즘은 해당 객체를 한 번만 저장하고, 이후 참조는 기록된 객체를 가리키도록 처리한다.
-
필터링 및 예외: 직렬화 과정에서 제외하고 싶은 객체의 필드에는
transient키워드를 붙일 수 있다. 이는 직렬화 시 무시되고, 역직렬화 시 해당 필드는 기본값으로 초기화된다. 그리고static필드는 객체에 속한 값이 아니라 클래스 자체에 속한 값이므로 직렬화 대상에서 자동으로 제외된다.
보안 문제 및 커스텀 직렬화
직렬화는 편리하지만, 보안 취약점과 커스텀 로직 적용의 필요성이 있다.
-
보안 취약점(RCE or RCX): 역직렬화는 바이트 스트림을 실행 가능한 객체로 변환하기 때문에, 공격자가 악의적인 바이트 스트림을 삽입하여 시스템에서 임의의 코드를 실행하도록 만들 수 있다. 따라서 신뢰할 수 없는 출처로부터 받은 바이트 스트림은 역직렬화하지 않도록 주의해야 한다.
-
커스텀 직렬화: 기본 직렬화가 아닌, 개발자가 직접 직렬화/역직렬화 로직을 제어하고 싶을 때는 두 가지 방법이 있다.
-
Externalizable 인터페이스:
Serializable대신java.io.Externalizable인터페이스를 구현하여, 개발자가 모든 저장 및 복원 로직을 직접 구현한다. 이는 더 복잡하지만, 더 많은 제어 권한과 높은 설정을 가질 수 있다. -
writeObject(), readObject() 메소드:
Serializable을 구현한 클래스 내에 특정 시그니처를 가진 두 메소드를 정의하면, JVM은 기본 직렬화 대신 이 메소드를 호출하여 부분적인 커스텀을 가능하게 한다.
-
직렬화의 대안
자바 직렬화는 자바에 종속적이라는 단점이 있어, 최근에는 다른 언어와의 호환성과 보안성을 위해 다른 데이터 직렬화 타입이 더 많이 사용된다. 이는 다음과 같다.
-
JSON(JavaScript Object Notation): 사람이 읽기 쉽고, 대부분의 프로그래밍 언어에서 지원하는 가장 일반적인 데이터 교환 타입이다. 대표적으로 REST API 통신에서 사용된다.
-
Protocol Buffers(Google): 바이너리 형태로 직렬화되어 JSON보다 작고 빠르다. 특정 스키마를 강제하여 데이터의 안전성을 높이며, 고성능 원격 프로시저 호출(RPC) 환경에서 사용된다.