Java의 Serialization(직렬화)은 네트워크를 통해 전송하거나, 파일로 저장하고, 나중에 사용하기 위해 DB에 저장하는 일들을 하기 위해 Object를 Stream으로 변환하게 도와줍니다. Deserialization(역직렬화) 는 Object Stream을 프로그램에서 사용할 Java 객체로 변환하는 과정을 말합니다.
Serialization(직렬화) 는 Java 시스템 내부에서 사용하는 객체 혹은 데이터 즉, JVM의 메모리에 상주(heap, stack)되어있는 객체 데이터를 외부의 자바 프로그램에서도 사용할 수 있도록 byte(바이트) 형태로 변환하는 기술입니다. Deserialization(역직렬화) 는 반대로 byte 형태의 데이터를 객체로 변환하는 기술입니다.
class 객체를 직렬화하려면 java.io.Serializable 인터페이스를 implements 하여 구현해야 합니다. Java의 Serializable인터페이스는 아무것도 구현할 필드나 메서드가 없는 빈 인터페이스(marker interface)입니다.
ObjectInputStream과 ObjectOutputStream이 Serialization을 구현하기 때문에, 네트워크를 통해 전송하거나 파일로 저장하기 위한 래퍼(wrapper)만 있으면 됩니다. 간단하게 java의 직렬화를 예로 들어보겠습니다.
import java.io.Serializable;
public class User implements Serializable {
// private static final long serialVersionUID = -15981268745621L;
private String name;
private int id;
transient private int age;
@Override
public String toString(){
return "User{name="+name+",id="+id+",age="+age+"}";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SerializationUtil {
// 파일을 객체로 역직렬화
public static Object deserialize(String fileName) throws IOException,
ClassNotFoundException {
FileInputStream fis = new FileInputStream(fileName);
ObjectInputStream ois = new ObjectInputStream(fis);
Object obj = ois.readObject();
ois.close();
return obj;
}
// 객체를 직렬화해서 파일로 저장
public static void serialize(Object obj, String fileName)
throws IOException {
FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
fos.close();
}
}
import java.io.IOException;
public class SerializationTest {
public static void main(String[] args) {
String fileName="user.serial";
User user = new User();
user.setId(123);
user.setName("senbro");
user.setAge(13);
//파일로 직렬화
try {
SerializationUtil.serialize(user, fileName);
} catch (IOException e) {
e.printStackTrace();
return;
}
User newUser = null;
try {
newUser = (User) SerializationUtil.deserialize(fileName);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
System.out.println("user Object - "+user);
System.out.println("newUser Object - "+newUser);
}
}
user Object - User{name=senbro, id=123, age=13}
newUser Object - User{name=senbro, id=123, age=0}
Java의 직렬화는 무시할 수 있는 수정이라면 수정하더라도 정상적으로 작동합니다. 무시할 수 있는 수정에는 다음과 같은 것들이 있습니다.
그러나 하나 허용되지 않는 수정이 있는데, 바로 serailVersionUID입니다. 이것을 증명하기 위한 테스트로 위의 예시에서 이미 직렬화된 파일을 역직렬화하는 코드를 구현해보겠습니다.
import java.io.IOException;
public class DeserializationTest {
public static void main(String[] args) {
String fileName="User.serial";
User newUser = null;
try {
newUser = (User) SerializationUtil.deserialize(fileName);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
System.out.println("newUser Object::"+newUser);
}
}
그리고 기존의 User Class에 password 변수와 그 변수에 대한 getter,setter 메서드를 추가해줍니다.
import java.io.Serializable;
public class User implements Serializable {
// private static final long serialVersionUID = -15981268745621L;
private String name;
private int id;
transient private int age;
private String password;
@Override
public String toString(){
return "User{name="+name+",id="+id+",age="+age+"}";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
java.io.InvalidClassException: com.senbro.test.Employee; local class incompatible: stream classdesc serialVersionUID = -15981268745621, local class serialVersionUID = -15321864285452
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:604)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369)
at com.senbro.test.SerializationUtil.deserialize(SerializationUtil.java:22)
at com.senbro.test.DeserializationTest.main(DeserializationTest.java:13)
newUser Object::null
에러가 발생하는 이유는 이전 class의 serialVersionUID와 새 class의 serialVersionUID가 달랐기 때문입니다. 직렬화를 하는 class의 serialVersionUID를 정의하지 않으면 컴파일 중 자동으로 계산되어 할당됩니다. Java는 변수, 메서드, 클래스명, 패키지 등을 이용하여 고유한 숫자를 만듭니다. 만약 IDE를 사용하는 경우에 “The serializable class Employee does not declare a static final serialVersionUID field of type long”라는 에러를 만나게 될 수 있습니다. 이때 serialVersionUID의 값은 정수값이면 어떤 값으로 지정해도 상관없지만 다른 클래스와 같은 값을 갖지 않도록 serialver.exe를 사용해서 생성된 값을 사용하는것이 보통입니다. seruakver.exe는 커맨드 창에서 실행해야하며 다음은 serialver.exe를 사용한 예시입니다.
....jdk\bin> serialver {classpath}.User
이렇게 하면 프로그램에 따로 serialVersionUID를 정의할 필요없이 우리가 원하는 값으로 설정할 수 있습니다. serialVersionUID는 새로운 class가 같은 class의 새로운 버전이고 가능한 역직렬화를 해야한다고 역직렬화 프로세스에게 알려주기 위해 존재하는 식별번호입니다.
예를들어 User class에서 serialVersionUID 필드의 주석을 제거하고 SerializationTest 프로그램을 실행합니다. 그리고 다시 DeserializationTest 프로그램을 실행하면 User class의 변경사항이 직렬화 프로세스와 호환되어 역직렬화도 성공하는것을 확인할 수 있습니다.
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class Person implements Externalizable{
private int id;
private String name;
private String gender;
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(id);
out.writeObject(name+"xyz");
out.writeObject("abc"+gender);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
id=in.readInt();
name=(String) in.readObject();
if(!name.endsWith("xyz")) throw new IOException("corrupted data");
name=name.substring(0, name.length()-3);
gender=(String) in.readObject();
if(!gender.startsWith("abc")) throw new IOException("corrupted data");
gender=gender.substring(3);
}
@Override
public String toString(){
return "Person{id="+id+",name="+name+",gender="+gender+"}";
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
}
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class ExternalizationTest {
public static void main(String[] args) {
String fileName = "person.serial";
Person person = new Person();
person.setId(1);
person.setName("senbro");
person.setGender("Male");
try {
FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(person);
oos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
FileInputStream fis;
try {
fis = new FileInputStream(fileName);
ObjectInputStream ois = new ObjectInputStream(fis);
Person p = (Person)ois.readObject();
ois.close();
System.out.println("Person Object Read="+p);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Person Object Read=Person{id=1,name=senbro,gender=Male}
다음편에 계속...
https://www.digitalocean.com/community/tutorials/serialization-in-java#serialVersionUID