본문 바로가기

Develop/JAVA

자바 객체 직렬화(Serialize), 역직렬화(Deserialize)란?

1. 직렬화란?

 

자바 직렬화란 자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부 자바 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 변환하는 기술(직렬화)과, 바이트로 변환된 데이터를 다시 객체로 변환하는 기술(역직렬화)를 아울러 이야기한다.

 

조금 더 기술적으로 설명하자면 JVM 메모리(힙 또는 스택)에 올라간 객체 데이터를 바이트 형태로 변환하는 기술과, 직렬화된 바이트 형태의 데이터를 객체로 변환해서 JVM으로 올리는 형태를 함께 이야기한다.

2.  직렬화를 하는 이유

생성한 객체를 파일로 저장하거나, 저장한 객체를 읽어야 하는 경우가 생길 수 있다.

또한 네트워크 통신을 통해 다른 서버에서 생성한 객체를 받아와야 할 수도 있다.

 

만약 이때 직렬화 과정을 거치지 않는다면 파싱이 가능한 유의미한 데이터를 주고 받을 수 없게 된다.

 

데이터의 메모리 구조는 프로그래밍 언어에 상관 없이 크게 2가지로 나뉜다.

1) 값 형식 데이터(Value Type) 2) 참조 형식 데이터(Reference Type)

 

이 중 저장/전송 가능한 데이터 구조는 값 형식 데이터이다.

참조 형식 데이터는 실제 데이터 값이 아니라, 힙 영역에 할당되어 있는 메모리 주소를 가지고 있기 때문에 저장/전송이 불가능하다.

 

예를 들어 class A의 인스턴스를 생성하고, 해당 인스턴스의 참조 주소값이 0x0x00121212을 가진다. 그리고 이 주소값을 파일에 포함하여 저장했다. 그 후 프로그램을 재실행하고 파일을 읽어서 0x0x00121212 주소를 가져오더라도 방금 만들었던 class A의 인스턴스를 가져올 수 없다. 왜냐하면 프로그램이 종료되는 순간 기존에 할당되었던 메모리는 해제되기 때문이다.

 

네트워크 통신의 경우도 마찬가지다. 각각의 서버는 물리적으로 사용중인 메모리 공간(OS의 가상메모리 포함)이 일치하지 않는다.

A -> B 서버로 참조 주소값을 전달하더라도, B 서버 입장에는 완전 엉뚱한 주소값을 전달 받은 꼴이 된다.

 

따라서 객체가 포함하고 있는 주소값의 실체를 모두 끌어모아서 값 형식 데이터로 변환하는 과정을 직렬화라고 한다!

이때 직렬화된 데이터 형식은 프로그래밍 언어에 따라 텍스트 또는 바이너리 형태를 가진다.

 

결과적으로 직렬화를 거쳐야만 데이터를 저장/전송했을 때 파싱 가능한 유의미한 데이터를 만들 수 있다.

참고: https://okky.kr/article/224715

3. 자바에서의 직렬화

자바 직렬화 외에도 데이터를 직렬화하는 다양한 방법들이 있다. 예를 들어 JSON, CSV, XML, 프로토콜 버퍼 등이 있다.

JSON, CSV, XML, 프로토콜 버퍼 등은 시스템의 고유 특성과 상관없는 범용적인 API나 데이터를 변환하여 추출할 때 많이 사용된다.

(표 형태의 다량의 데이터 직렬화에는 CSV, 구조적인 데이터는 XML, JSON이 주로 사용)

1) 자바 직렬화의 장점

그렇다면 이렇게 범용적으로 사용할 수 있는 데이터 포맷들이 있는데도 자바 직렬화를 사용하는 이유는 간단하다.

자바 직렬화는 이름에서 알 수 있듯이 자바 시스템에서 개발에 최적화되어 있다.

복잡한 데이터 구조를 가진 클래스의 객체라도, 기본 조건만 지키면 큰 작업 없이 바로 직렬화가 가능하다. (역직렬화도 동일)

데이터 타입을 자동으로 맞춰주기 때문에 개발자 입장에서는 큰 신경을 쓰지 않고 편하게 개발할 수가 있다는 장점이 있다.

 

따라서 자바 직렬화는 JVM 메모리에서만 상주되어 있는 객체 데이터를 그대로 영속화가 필요할 때 사용된다.

영속화된 데이터는 시스템이 종료되더라도 없어지지 않는 장점이 있고, 네트워크로 전송도 가능하다.

영속화는 단순하게 설명하면 애플리케이션의 데이터가 애플리케이션의 프로세스보다 더 오래 지속하게끔 하려는 것이다. Java 용어로 말하면, 객체의 상태가 JVM의 범위를 넘어서서 지속하여 추후에 동일한 상태를 이용하려는 것이다.

2) 자바 직렬화의 단점

아래에서 살펴보겠지만 자바 직렬화는 상당히 간편하게 구현할 수 있다. (java.io.Serializable 인터페이스 구현)

하지만 생각보다 단점이 많기 때문에 Serializable 구현을 신중하게 결정해야 한다. (Effective Java 3/E 아이템 86)

 

자바 직렬화는 내부적으로 SUID(serialVersionUID) 값을 활용한다.

만약 개발자가 해당 값을 명시적으로 작성하지 않았더라도, 해당 클래스 구조 정보를 이용하여 생성한 해쉬 값을 사용한다.

따라서 객체를 직렬화한 뒤 클래스 구조 정보가 중간에 변경된다면, 역직렬화 시 java.io.InvalidClassException 예외가 발생한다.

이유는, serialVersionUID의 정보가 일치하지 않기 때문이다!

public class Member implements Serializable {
  private static final long serialVersionUID = 1L;
  ...
}

따라서 다음과 같이 serialVersionUID 값을 직접 관리해서 혼란을 피할 수 있다.

이제 멤버 변수가 중간에 제거된다면 에러가 발생하지 않고 해당 값 자체만 없어진다. 반대로 멤버 변수가 추가된다면 원하는 형태로 값이 채워지게 된다.

 

하지만 모든 문제가 해결되는 건 아니다.

자바 직렬화는 상당히 타입에도 엄격하다. 예를 들어 멤버 변수의 타입을 String에서 StringBuilder로 바꾼다면 ClassCastException이 발생한다. 마찬가지로 Primitive 타입인 int에서 long으로 바꾸더라도 InvalidClassException이 발생한다.

 

따라서 자바 직렬화는 신중하게 결정해야 한다. 만약 자바 직렬화를 구현하기로 결정했다면 멤버 변수 타입 변경을 지양해야 하고, 외부(DB, 캐시 서버 등)에 장기간 저장될 정보는 자바 직렬화를 피해야 한다. 역직렬화 대상 클래스가 언제 변경될지 모르는 환경에서 긴 시간 외부에 존재했던 직렬화 데이터는 쓰레기(Garbage)가 될 가능성이 높다. 이런 경우 에러가 언제 발생해도 이상하지 않다.

 

마지막으로 자바 직렬화시에 기본적으로 타입에 대한 정보 등 클래스의 메타 정보도 가지고 있기 때문에 상대적으로 다른 포맷에 비해서 용량이 큰 문제도 존재한다. 특히 클래스의 구조가 거대해지면 용량 차이가 더욱 커진다. 내부에 참조하고 있는 모든 클래스에 대한 메타정보를 가지고 있기 때문이다.

참고)
https://techblog.woowahan.com/2550/
https://techblog.woowahan.com/2551/
https://brunch.co.kr/@oemilk/179
https://devlog-wjdrbs96.tistory.com/268
https://luckydavekim.github.io/development/back-end/java/serialization-java

4. 자바 직렬화 구현 방법