직렬화(Serialization), 역직렬화(Deserialization)
•
직렬화
◦
객체들의 연속적인 데이터(스트림)로 변환하여 전송 가능한 형태로 만드는 것을 의미한다.
•
역직렬화
◦
직렬화된 데이터를 다시 객체의 형태로 만드는 것을 의미한다.
객체 데이터를 통신하기 쉬운 포맷(Byte, CSV, Json..) 형태로 만들어주는 작업을 직렬화라고 볼 수 있고, 역으로, 포맷(Byte, CSV, Json…) 형태에서 객체로 변환하는 과정을 역직렬화라고 할 수 있다.
다음과 같은 클래스가 있다고 하자.
class Person {
private String name;
public Sample(String name) {
this.name = name;
}
}
Java
복사
•
Json 데이터 형식을 예로 들면 Person person = new Person(”나인영”) 객체를 {”name” : “나인영”} 와 같은 방식으로 변경하는 것을 직렬화라고 한다.
직렬화가 필요한 이유는?
자바에는 원시타입(Primitive Type)이 byte, short, int, long, float, double, boolean, char 총 8가지가 있다. 그 외 객체들은 주소값을 갖는 참조형 타입이다.
원시타입은 stack에서 값 그 자체로 갖고있어 외부로 데이터를 전달할 때, 값을 일정한 형식의 raw byte 형태로 변경하여 전달할 수 있다. 하지만 위 그림과 같이 객체의 경우 실제로 Heap 영역에 존재하며 Stack 에서는 Heap 영역에 존재하는 객체의 주소(메모리 주소)를 가지고 있다.
만약에 이 주소 값을 다른 곳에 보낸다면 어떻게 될까? 먼저 프로그램이 종료되거나 객체가 쓸모없다고 판단되면 Heap 영역에 있는 데이터는 제거된다. 따라서 본인 메모리에서도 데이터가 사라진다. 외부로 전송했다고 생각했을 때에도, 전송받은 기기의 전달받은 메모리 주소에 내가 전송하려고 했던 데이터가 있을리가 없다.
따라서 이 주소값의 데이터(실체)를 Primitive 한 값 형식 데이터로 변환하는 작업을 거친 다음에 전달해야 한다. 그래야 파일 저장이나 네트워크 전송시 파싱할 수 있는 유의미한 데이터로 변환된다.
직렬화를 하는 방법
•
Serializable 인터페이스 구현
class Sample implements Serializable {
}
class Sample2 extends Sample {
}
Java
복사
직렬화가 가능한 클래스를 만들기 위해선 Serializable 인터페이스를 구현하도록 하면 된다. 혹은 Serializable 인터페이스를 구현한 클래스를 상속받으면 된다.
class Sample implements Serializable {
transient String name;
}
Java
복사
특정 필드를 직렬화하고 싶지 않은 경우 transient 키워드를 붙이면, 그 타입의 기본값(int 인 경우 0, 객체인 경우 null)으로 직렬화된다.
만약, 직렬화 할 수 없는(Serializable 을 구현하지 않는) 객체를 필드 멤버로서 갖고 있다면, java.io.InvalidClassException 예외가 발생하여 직렬화 할 수 없다.
실습
import java.io.*;
import java.util.Base64;
class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("나인영", 19);
byte[] serializedPerson;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(person);
serializedPerson = baos.toByteArray();
}
}
// 바이트 배열로 생성된 직렬화 데이터를 base64로 변환
System.out.println(Base64.getEncoder().encodeToString(serializedPerson));
try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedPerson)) {
try (ObjectInputStream ois = new ObjectInputStream(bais)) {
Object objectPerson = ois.readObject();
Person newPerson = (Person) objectPerson;
System.out.println(newPerson);
}
}
}
}
class Person implements Serializable {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Java
복사
위와 같이 name = “나인영” age = 19인 Person 객체를 OutputStream 클래스를 직렬화하고 직렬화한 객체를 다시 InputStream 클래스로 역직렬화 하였다.
// 결과
rO0ABXNyAAZQZXJzb26JVHYPpINL8gIAAkkAA2FnZUwABG5hbWV0ABJMamF2YS9sYW5nL1N0cmluZzt4cAAAABN0AAnrgpjsnbjsmIE=
Person{name='나인영', age=19}
Java
복사
실제로 생성된 byte 배열을 출력해보면
[-84, -19, 0, 5, 115, 114, 0, 6, 80, 101, 114, 115, 111, 110, -119, 84, 118, 15, -92, -125, 75, -14, 2, 0, 2, 73, 0, 3, 97, 103, 101, 76, 0, 4, 110, 97, 109, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 120, 112, 0, 0, 0, 19, 116, 0, 9, -21, -126, -104, -20, -99, -72, -20, -104, -127]
Java
복사
엄청나게 긴 배열이 나온다.
타입에 대한 정보 등 클래스의 메타 정보 역시 가지고 있기 때문에 Json 처럼 최소한의 메타정보만 갖는 포맷보다 데이터가 더 많아진다.
!! 참고로, 직렬화 시킨 결과물이 json 형태로 직렬화 한것과 비교했을 때, 훨씬 용량이 크다는 점이 자바 직렬화의 단점 중 하나이다.
저 위의 byte 배열을 그대로 넣고 역직렬화 시켜보자. 직렬화가 잘 된 데이터라면 역직렬화도 잘 되어야 할 것이다.
class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("나인영", 19);
byte[] serializedPerson =
{-84, -19, 0, 5, 115, 114, 0, 6, 80, 101, 114, 115, 111, 110, -119, 84, 118, 15, -92,
-125, 75, -14, 2, 0, 2, 73, 0, 3, 97, 103, 101, 76, 0, 4, 110, 97, 109, 101, 116,
0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103,
59, 120, 112, 0, 0, 0, 19, 116, 0, 9, -21, -126, -104, -20, -99, -72, -20, -104, -127};
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(person);
serializedPerson = baos.toByteArray();
}
}
try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedPerson)) {
try (ObjectInputStream ois = new ObjectInputStream(bais)) {
Object objectPerson = ois.readObject();
Person newPerson = (Person) objectPerson;
System.out.println(newPerson);
}
}
}
}
Java
복사
위 코드를 실행시키면 역직렬화가 잘 된다. 이로써 언젠가는 사라져버릴 인스턴스 상태에서 영속화가 되었다.
Person{name='나인영', age=19}
Java
복사
직렬화 주의점
그렇다면 만약 직렬화시킨 byte 배열을 그대로 갖고 있는 상태에서 Person 클래스 멤버 변수의 타입이 변경되거나 이름이 변경되어 새로운 멤버 변수가 추가되면 어떻게 될 것인가?
local class incompatible: stream classdesc serialVersionUID = 6576246705851303384, local class serialVersionUID = 2808296220477750099
Java
복사
테스트 결과, 역직렬화 과정에서 위와 같은 에러가 발생한다. 에러 메시지를 확인해보면, serialVersionUID 가 일치하지 않는다는 의미인데, serialVersionUID 를 따로 명시해주지 않으면 클래스의 기본 해쉬값을 사용하게 된다.
따라서, Person 클래스에 변화가 생겨 serialVersionUID 도 새로운 값으로 변경되어 위와 같은 에러가 발생한 것이다.
class Person implements Serializable {
private static final long serialVersionUID = 1L;
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Java
복사
위와 같이 serialVersionUID 를 명시해준 뒤, 직렬화를 다시 한 다음 Person 에 새로운 멤버 변수를 추가해서 다시 역직렬화를 시도하면 에러가 발생하지 않음을 확인할 수 있다. 기존에 있던 멤버 변수를 삭제해도 에러가 발생하지 않는다.
단, 새로운 필드가 추가되는 것은 무시하는 것으로 에러를 일으키지 않지만, 기존에 있던 age가 int 타입에서 long 타입으로 변경되면
Exception in thread "main" java.io.InvalidClassException: test.Person; incompatible types for field age
Java
복사
다시 에러를 일으킨다.
자바에서 제공하는 직렬화 기능은 추가적인 라이브러리 설치 없이 객체 데이터를 영속화시킬 수 있다는 장점이 있지만, 직렬화 결과물 용량이 상대적으로 커서 비효율적인 문제가 있다. 자주 변경될 수 있는 데이터를 직렬화하여 보관하게 되면 나중에 변경이 생겼을 때 역직렬화가 불가능해지므로 쓸모없는 데이터가 될 수 있는 단점이 있다. 그러므로 직렬화는 반드시 자주 변경되지 않는 데이터일 때 진행하는 게 좋다.
참고링크: