자바는 메모리에 생성된 객체를 파일 또는 네트워크로 출력할 수가 있다.
객체는 문자가 아니기 때문에 바이트 기반 스트림으로 출력해야 한다. 객체를 출력하기 위해서는
객체의 데이터(필드값)를 일렬로 늘어선 연속적인 바이트로 변경해야 하는데,
이를 직렬화(serialization)이라고 한다.
반대로 파일에 저장되어 있거나 네트워크에서 전송된 객체를 읽을 수도 있는데,
입력 스트림으로부터 읽어들인 연속적인 바이트를 객체로 복원하는 것을 역직렬화(deserialization)이라 한다.
Serializable
자바는 Serializable 인터페이스를 구현한 클래스만 직렬화 할 수 있도록 제한한다.
package java.io;
public interface Serializable {
}
io패키지에 있는 인터페이스로서 메소드와 필드가 없다.
단지, 객체를 직렬화 해도 좋다는 표시 역할을 한다.
객체를 직렬화하면 바이트로 변환되는 것은 필드들이고, 생서자 및 메소드는 직렬화에 포함되지 않는다.
따라서 역직렬화 할 때에는 필드의 값만 복원된다.
모든 필드가 직렬화 되는 건 아니고 static 또는 transient 키워드가 붙은 필드는 직렬화 대상에서 제외된다.
Serializable을 상속받지 않는 객체를 직렬화하면 NotSerializableException이 발생한다.
※transient: 직렬화에서 제외하는 용도로 쓰이는 키워드. 다른곳에서는 쓰이지 않는다.
serailVersionUID 필드
serialVersionUID는 같은 직렬화됐던 객체와 역질렬화 하려는 객체가 같은 클래스임을 알려주는 식별자 역할을 한다.
IDE의 코드상에는 보이지 않지만, Serializable 상속한 클래스를 컴파일하면 자동적으로
serialVersionUID 정적필드가 추가된다.
문제는 serialVersionUID는 컴파일 할 때마다 값이 달라진다는 것이다.
(개발환경에 따라 코드내용에 변경이 없이 재 컴파일 하는 경우는 serialVersionUID 값이 안 변할 수도 있다.
그럴 때는 코드 내용을 변경하고 컴파일 해보자. )
다음의 예제에서 한번에 수정 없이 ObjectOutputEx, ObjectIntputEx를 순차적으로 실행한다면
아무 문제없이 실행될 것이다.
ClassA (직렬화,역직렬화 될 클래스)
import java.io.Serializable;
public class ClassA implements Serializable {
int field1;
}
ObjectOutputEx
import java.io.*;
public class ObjectOutputEx {
public static void main(String[] args) throws Exception {
FileOutputStream fos = new FileOutputStream("C:/workspace/temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
ClassA classAOriginal = new ClassA();
classAOriginal.field1 = 1;
oos.writeObject(classAOriginal);
oos.flush();
oos.close();
fos.close();
}
}
실행 후 다음과 같이 파일이 생긴다
ObjectInputEx
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class ObjectInputEx {
public static void main(String[] args) throws Exception{
FileInputStream fis= new FileInputStream("C:/workspace/temp.txt");
ObjectInputStream ois=new ObjectInputStream(fis);
ClassA classAFromFile=(ClassA) ois.readObject();
System.out.println("classAFromFile.field = " + classAFromFile.field1);
}
}
실행결과 콘솔에 다음과 같이 출력된다.
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
현재 직렬화 된 객체(ClassA)가 파일에 저장되어있다. (C:/workspace/temp.txt)
이번엔 ClassA를 다음과 같이 변경해보자.
ClassA.
import java.io.Serializable;
public class ClassA implements Serializable {
int field1;
int field2;
}
Exception in thread "main" java.io.InvalidClassException: ClassA; local class incompatible: stream classdesc serialVersionUID = 3718886090764968835, local class serialVersionUID = -1041639892687008082
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2001)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1848)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2158)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
at ObjectInputEx.main(ObjectInputEx.java:10)
이 에러는 ObjectOutputEx에서 처음 직렬화 할 때(파일에 저장되어 있는 객체)의
serialVersionUID는 3718886090764968835 인데
ClassA 변경 이후에 ClassA의 serialVersionUID의 값은 -1041639892687008082 로
그 값이 서로 달라서 생기는 에러이다.
이번엔 처음 직렬화 된 serailVersionUID값인 3718886090764968835 을 ClassA에 적어주자.
(이 값은 사용자에 따라 다를 수 있다)
ClassA
import java.io.Serializable;
public class ClassA implements Serializable {
static final long serialVersionUID=3718886090764968835L;
int field1;
int field2;
}
이러면 이 ClassA의 serialVersionUID값은 3718886090764968835로 고정된다.
이 후 다시 ObjectInputEx를 실행하면
이번에는 serialVersionUID값이 서로 같기 때문에 정상적으로 실행된다.
이처럼 직렬화 대상 클래스(ClassA)가 변경 될 가능성이 있다면 serailVersionUID를 명시적으로 선언해주면
그 값이 고정되서 재 컴파일 된 이후에도 여전히 역질렬화가 가능합니다.
※직렬화 하는 프로그램과 역질렬화 프로그램이 항상 같지 않을 수 있습니다.
여러대의 서버가 있는 경우 서버 A에서 직렬화해서 만들어 놓은 파일을 서버B에서 역직렬화 할 수도 있습니다.
직렬화 대상 클래스(ClassA)가 서버A와 서버B에 완전히 똑같은 코드로 있어도
JVM은 서버A에 대해 컴파일, 서버 B에 대해 컴파일, 이렇게 따로 컴파일 해서 serialVersionUID가 다를 수 있습니다.
이럴 때는 꼭 serailVersionUID를 명시해줘야 합니다.