객체를 컴퓨터에 저장했다가 꺼내쓰는것과 컴퓨터간에 서로 객체를 주고받을 수 있게 해 주는것을 직렬화를 통해 가능하게한다.
직렬화(serialization)란 객체를 데이터 스트림으로 만드는 것을 뜻한다. 객체에 저장된 데이터를 스트림에 쓰기(write)위해 연속적인(serial) 데이터로 변환하는 것을 말한다. 반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화(deserialization)이라고 한다.
객체는 클래스에 정의된 인스턴스변수의 집합이다. 객체는 클래스변수나 메서드가 포함되지 않는다. 객체는 오직 인스턴스변수들로만 구성되어 있다. 인스턴스변수는 인스턴스마다 다른 값을 가질 수 있어야하기 때문에 별도의 메모리공간이 필요하지만 메서드는 변하는 것이 아니라 인스턴스마다 같은 내용의 코드를 포함시킬 이유는 없다.
객체를 저장한다는 것은 바로 객체의 모든 인스턴스변수의 값을 저장한다는 것이다. 어떤 객체를 저장하고자 한다면, 현재 객체의 모든 인스턴스 변수의값을 저장하기만 하면 된다. 그리고 저장했던 객체를 다시 생성하려면, 객체를 생성한 후에 저장했던 값을 읽어서 생성한 객체의 인스턴스변수에 저장하면 되는 것이다.
클래스에 정의된 인스턴스변수가 단순히 기본형일 때는 인스턴스변수의 값을 저장하는 일이 간단하지만, 인스턴스변수의 타입이 참조형 일 때는 그리 간단하지 않다. 그러나 우리에게는 직렬화/역직렬화를 할 수 있는 ObjectInputStream/ObjectOutputStream을 사용하는 방법만 알면된다.
두 객체가 동일한지 판단하는 기준이 두 객체의 인스턴스변수의 값들이 같고 다름이라는 것을 기억하자.
ObjectInputStream, ObjectOutputStream
직렬화(스트림에 객체를 출력)에는 ObjectOutputStream을 사용하고 역직렬화(스트림으로부터 객체를 입력)에는 ObjectInputStream을 사용한다.
ObjectInputStream과 ObjectOutputStream은 각각 InputStream과 OutputStream을 직접 상속받지만 기반스트림을 필요로하는 보조스트림이다. 그래서 객체를 생성할 때 입출력(직렬화/역직렬화)할 스트림을 지정해 주어야 한다.
ObjectInputStream(InputStream in)
ObjectOutputStream(OutputStream out)
만일 파일에 객체를 저장(직렬화)하고 싶다면 다음과 같이 하면 된다.
FileOutputStream fos = new FileOutputStream("objectfile.ser");
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(new UserInfo());
위의 코드는 objectfile.ser이라는 파일에 UserInfo객체를 직렬화하여 저장한다. 출력할 스트림(FileOutputStream)을 생성해서 이를 기반스트림으로하는 ObjectOutputStream을 생성한다.
ObjectOutputStream의 writeObject(Object obj)를 사용해서 객체를 출력하면, 객체가 파일에 직렬화되어 저장된다.
역직렬화 방법은 입력스트림을 사용하고 wrieteObject(Object obj)대신 readObject()이기 때문에 객체 원래의 타입으로 형변환 해주어야 한다.
ObjectInputStream과 ObjectOutputStream에는 readObject()와 writeObject()이외에도 여러 가지 타입과 값을 입출력할 수 있는 메서드를 제공한다.
ObjectInputStream | ObjectOutputStream |
void int int boolean byte char double float int long short Object int int Object string |
defaultReadObject() read() read(byte[]buf, int off, int len) readboolean() readByte() readChar() readDouble() readFloat() readInt() readLong() readShort() readObject() readUnsignedByte() readUnsignedShort() readUnshared() readUTF() |
void defaultWriteObject void write(byte[] buf) void write(byte[] buf, int off, int len) void write(int val) void writeBoolean(boolean val) void writeByte(int val) void writeBytes(String str) void writeChar(int val) void writeChars(String str) void writeDouble(double val) void writeFloat(float val) void writeInt(int val) void writeLong(long val) void writeObject(Object obj) void writeShort(int val) void writeUnshared(Object obj) void write UTF(String str) |
이 메서드들은 직렬화와 역직렬화를 직접 구현할 때 주로 사용되며, defaultReadObject(0와 defaultWriteObject()는 자동 직렬화를 수행한다. 객체를 직렬화/역직렬화하는 작업은 객체의 모든 인스턴스변수가 참조하고 있는 모든 객체에 대한 것이기 때문에 상당히 복잡하며 시간도 오래 걸린다. readObject()와 writeObject()를 사용한 자동 직렬화가 편리하기는 하지만 직렬화 작업시간을 단축시키려면 직렬화하고자 하는 객체의 클래스에 추가적으로 다음과 같은 2개의 메서드를 직접 구현해주어야 한다.
private void writeObject(ObjectOutputStream out)
throws IOException {
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundExceaption{
}
직렬화가 가능한 클래스 만들기 - Serializable, transient
직렬화가 가능한 클래스를 만드는 방법은 간단하다. 직렬화하고자 하는 클래스가 java.io.Serializable인터페이스를 구현하도록 하면 된다.
예를 들어 왼쪽과 같이 UserInfo라는 클래스가 있을 때, 이 클래스를 직렬화가 가능하도록 변경하려면 오른쪽과 같이 Serializable인터페이스를 구현하도록 변경하면 된다.
public class UserInfo {
String name;
String password;
int age;
}
public class UserInfo
implements java.io.Serializable {
String name;
String password;
int age;
}
Serializable 인터페이스는 아무런 내요도 없는 빈 인터페이스이지만, 직렬화를 고려하여 작성한 클래스인지를 판단하는 기준이 된다.
public interface Serialzable{ }
아래와 같이 Serializable을 구현한 클래스를 상속받는다면, Serializable을 구현하지 않아도 된다. UserInfo는 Serializable을 구현하지 않았지만 조상인 SuperUserInfo가 Serializable를 구현하였으므로 UserInfo역시 직렬화가 가능하다.
public class SuperUserInfo implement Serializable {
String name;
String password;
}
public class UserInfo extends SuperUserInfo{
int age;
}
위의 경우 UserInfo객체를 직렬화하면 조상인 SuperUserInfo에 정의된 인스턴스변수name, password도 함께 직렬화된다.
그러나 조상클래스가 Serializable을 구현하지 않았다면 자손클래스를 직렬화할 때 조상클래스에 정의된 인스턴스변수 name과 password는 직렬화 대상에서 제외된다.
public class SuperUserInfo {
String name;
String password;
}
public class UserInfo extends SuperUserInfo implements Serializable {
int age;
}
조상클래스에 정의된 인스턴스변수 name과 password를 직렬화 대상에 포함시키기 위해서는 조상클래스가 Serializable을 구현하도록 하던가, UserIInfo에서 조상의 인스턴스변수들이 직렬화되도록 처리하는 코드를 직접 추가해 주어야 한다.
public class UserInfo implements Serializable {
String name;
String password;
int age;
Object obj = new Object(); // Object객체는 직렬화할 수 없다.
}
이 경우에는 직렬화 될 수 없다.
public class UserInfo implements Serializable {
String name;
String password;
int age;
Object obj = new String("abc"); // String은 직렬화될 수 없다.
}
인스턴스변수 obj의 타입이 직렬화가 안 되는 Objct이긴 하지만 실제로 저장된 객체는 직렬화가 가능한 String인스턴스이기 때문에 직렬화가 가능하다.
직렬화하고자 하는 객체의 클래스에 직렬화가 안 되는 객체에 대한 참조를 포함하고 있다면, 제어자 transient를 붙여서 직렬화 대상에서 제외되도록 할 수 있다.
또는 password와 같이 보안상 직렬화되면 안 되는 값에 대해서 transient를 사용할 수 있다. 다르게 표현하면 transient가 붙은 인스턴스변수의 값은 그 타입의 기본값으로 직렬화된다고 볼 수 있다.
즉, UserInfo객체를 역직렬화하면 참조변수인 obj와 password의 값은 null이 된다.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
class SuperUserInfo{
String name;
String password;
public SuperUserInfo(){
this("Unknown", "1111");
}
public SuperUserInfo(String name, String password){
this.name = name;
this.password = password;
}
}
public class UserInfo2 extends SuperUserInfo implements Serializable{
/**
*
*/
private static final long serialVersionUID = -7739307153548748307L;
int age;
String addr;
public UserInfo2() {
this("Unknown", "1111",0);
}
public UserInfo2(String name, String password, int age) {
super(name, password);
this.age = age;
}
@Override
public String toString() {
return "UserInfo2 [age=" + age + ", name=" + name + ", password=" + password + "]";
}
private void writeObject(ObjectOutputStream oos) throws IOException{
oos.writeUTF(name);
oos.writeUTF(password);
oos.defaultWriteObject();
}
private void readObject(ObjectInputStream ois) throws Exception {
name = ois.readUTF();
password = ois.readUTF();
ois.defaultReadObject();
}
public static void main(String[] args) throws Exception {
List<UserInfo2> list = new ArrayList<UserInfo2>();
list.add(new UserInfo2("1길동", "1111", 10));
list.add(new UserInfo2("2길동", "1212", 20));
new ObjectOutputStream(new FileOutputStream("abcd.ser")).writeObject(list);
List<UserInfo2> list2 = (List<UserInfo2>) new ObjectInputStream(new FileInputStream("abcd.ser")).readObject();
list2.forEach(System.out::println);
}
}
아래는writeObject()와 readObject()를 추가해서 조상으로부터 상속받은 인스턴스변수인 name과 password가 직접 직렬화되도록 해야 한다. 이 메서드들은 직렬화/역직렬화 작업시에 자동적으로 호출된다.
직렬화가능한 클래스의 버전관리
직렬화된 객체를 역직렬화할 때는 직렬화 했을 때와 같은 클래스를 사용해야한다. 그러나 클래스의 이름이 같더라도 클래스의 내용이 변경된 경우 역직렬화는 실패한다.
역직렬화를 이용해 클래스의 버전을 비교함으로써 직렬화할 때의 클래스 버전과 일치하는지 확인할 수 있다. 그러나 static변수나 상수 transient가 붙은 인스턴스변수가 추가되는 경우에는 직렬화에 영향을 미치지 않기 때문에 클래스의 버전을 다르게 인식하도록 할 필요는 없다.