[Java] 직렬화(Serialization)와 역직렬화(Deserialization), Serializable 인터페이스
자바에서의 직렬화(Serialization)과 역직렬화(DeSerialization)
클래스를 만들 때, 해당 특정 클래스에서 사용될 객체를 만들 수 있으며 프로그램을 실행 / 종료하면 가비지 수집기 스레드를 통해 객체가 자체적으로 수거됩니다. 객체를 다시만들지 않고 해당 클래스를 호출하려는 경우 어떻게 될까요?? 이럴때 데이터를 바이트 스트림으로 변환하여 직렬화 개념을 사용합니다.
객체 직렬화는 객체의 상태를 바이트 스트림으로 변환하는데 사용되는 프로세스로, 디스크 / 파일로 유지되거나 네트워크 통신을 통해 실행중인 다른 Java 가상머신으로 전송될 수 있습니다. 역직렬화란 이러한 바이트 스트림을 다시 객체형태로 변환하는 작업을 말합니다. 생성된 바이트는 플랫폼에 독립적입니다. 그래서 한 플랫폼에서 직렬화된 객체는 다른 플랫폼에서 역직렬화를 통해 데이터를 받아올 수 있습니다.
자바클래스 직렬화를 가능하게하는 방법
직렬화는 생성한 클래스를 Serializable 인터페이스를 구현하는 클래스로 만듬으로써 가능합니다. Serializable 인터페이스는 메소드도 필드도 없는 마킹역할을 하는 인터페이스입니다.
직렬화할 수 없는 객체를 직렬화하려고 하는 경우는 어떻게 될까요??
“RuntimeException: Exception in thread “main” java.io.NotSerializableException: java.io.ObjectOuputStream.” |
=> 직렬화 할 수 없습니다.
serialVersionUID라는 것은 무엇인가요??
serialVersionUID는 객체의 해시코드로 직렬화될 때 객체에 표시되는 식별자 역할을 합니다. 예컨데 serialver와 같은 툴로 객체의 serialVersionUID를 알아낼 수 있습니다.
serialVersionUID는 객체의 버전관리에 사용됩니다. serialVersionUID를 지정하지 않으면 클래스에서 필드를 추가하거나 수정할 때 serialVersionUID가 새 클래스에 대해 생성되고 이전 직렬화된 객체가 다르기 때문에 이미 직렬화된 객체를 복구할 수 없습니다. Java 직렬화 프로세스는 직렬화된 객체의 상태를 복구하기 위해 올바른 serialVersionUID를 사용하고 serialVersionUID가 일치하지 않는 경우 java.io.InvalidClassException을 발생시킵니다.
이에 대한 예제를 아래쪽에서 확인할 수 있습니다.
Example Coding
Member 클래스(직렬화 대상)
class Member implements Serializable {
private String name;
private String email;
private int age;
public Member(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
@Override
public String toString() {
return String.format("Member{name='%s', email='%s', age='%s'}", name, email, age);
}
}
직렬화(Serialization) 예제
package project;
import java.io.*;
import java.util.Base64;
class Scratch {
public static void main(String[] args) {
String byteString = serialize();
System.out.println(byteString);
}
private static String serialize() {
Member member = new Member("goodDabang", "wsh0821@station3.co.kr", 27);
byte[] serializedMember;
String serializedMemberStr = "";
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(member);
serializedMember = baos.toByteArray();
serializedMemberStr = Base64.getEncoder().encodeToString(serializedMember);
}
} catch (Exception e) {
e.printStackTrace();
}
return serializedMemberStr;
}
}
- Member -> byte Array -> encoding -> 해시문자열
- 해시된 문자열을 추후 deserialization을 통해 member 객체로 얻어올 수 있음.
역직렬화(Deserialization) 예제
package project;
import java.io.*;
import java.util.Base64;
class Scratch {
public static void main(String[] args) {
String serializedMemberStr = "";// 이전에 얻은 serializedMemberStr
byte[] serializedMember = Base64.getDecoder().decode(serializedMemberStr);
deserialize(serializedMember);
}
private static void deserialize(byte[] serializedMember) {
try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedMember)) {
try (ObjectInputStream ois = new ObjectInputStream(bais)) {
Object o = ois.readObject();
Member o1 = (Member) o;
System.out.println(o1);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 아까 출력했던 해시문자열을 디코더에 넣어줍니다. -> 그러면 다시 byte Array 를 얻을 수 있고, 바이트 스트림으로 객체를 읽어올 수 있습니다.
- ObjectInputStream 객체가 읽어들인 byte Array를 Object 형으로 뱉어냅니다. 이를 다시 캐스팅해서 member 객체를 얻어올 수 있습니다.
SerialVersionUID 사용하기
위의 예제에 이어서 Member 클래스의 field를 하나 추가해보겠습니다.
필드가 추가된 Member 클래스
class Member implements Serializable {
private String name;
private String email;
private int age;
private int additionalField;
public Member(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
@Override
public String toString() {
return String.format("Member{name='%s', email='%s', age='%s'}", name, email, age);
}
}
이전에 직렬화 했던 해시문자열을 다시 역직렬화 해보면..?
InvalidClassException이 발생한다. 즉, additionalField를 추가하기 이전의 Member 클래스와 이후의 Member 클래스를 별도의 다른 클래스로 여기기 때문에 발생하는 문제이다.
여기서 ID 식별자로써 역할을 하는 것이 SerialVersionUID이다. Member 클래스에 다음과 같이 SerialVersionUID를 추가하였고 위의 과정을 다시 짚어보았다.
직렬화 식별자를 가진 Member 클래스
class Member implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String email;
private int age;
private int additionalField;
public Member(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
@Override
public String toString() {
return String.format("Member{name='%s', email='%s', age='%s'}", name, email, age);
}
}
>> Member{name='goodDabang', email='wsh0821@station3.co.kr', age='27'}
새로운 컬럼이 추가되어도 InvalidClassException을 발생시키지 않는다. Member 클래스에 있는 serialVersionUID와 직렬화할때의 serialVersionUID의 값만 일치시키면 된다! 키니까!