10장. 직렬화

* 직렬화

  • 직렬화 : 객체를 바이트 스트림으로 바꾸는 작업을 직렬화(Serialization)라고 한다.
  • 자바에서는 자바에서 제공하는 기본적인 데이터 유형 이외에도 여러 객체들을 스트림에 쓰고, 읽을 수 있는 기능을 제공하는데 이것이 바로 객체 직렬화를 통해서 가능하다.
    이러한 객체 직렬화 기법은 원격 메소드 호출, 보안 및 암호화 등 실제 프로그래밍 시에 유용하게 사용되어 질 수 있다.

* 직렬화 과정

  • 먼저 객체들은 ObjectOutputStream에 의해서 직렬화가 되며, ObjectInputStream에 의해서 직렬화가 해제된다
  • 객체는 ObjectOutputStream의 writeObject() 메쏘드에 자신을 넘김으로써 직렬화 된다.
  • WirteObject()메쏘드는 private 필드와 super 클래스로부터 상속받은 필드를 포함, 객체의 모든 것을 기록하게된다.
  • 직렬화 해제는 직렬화와 반대의 과정을 거치게 되는데 ObjectInputStream의 readObject()메쏘드를 호출함으로써 스트림으로부터 읽어 들이고 이를 직렬화가 되기전의 객체로 다시 만들게 된다

10-1. Serializable 인터페이스는 신중하게 Implements하라

10-1-1. Serializable 인터페이스 를 Implements시 고려사항

  • 1. 어떤 클래스가 Serializable 인터페이스를 implements 하고 나면, 일단 이 클래스가 배포된 후에 구현을 바꾸는 데 비용이 매우 커진다.
  • 2. Serializable 인터페이스를 Implements 할 때, 두 번째로 고려해야 할 것은 버그와 보안 구멍이 생길 가능성이 커진다는 것이다.
  • 3. 새 버전의 클래스를 배포할 때 시험해야 할 것이 많아 진다

10-1-2. Serializable 인터페이스 를 Implements 하지 않기로 하였을시 주의할 점

  • 1. 상속을 위해 설계한 클래스가 Serializable 인터페이스를 implements 하지 않으면 이 클래스의 모든 하위 클래스들의 인스턴스를 직렬화시킬 수 없을지도 모른다.
    (상위 클래스에 접근할 수 있는 기본 생성자도 없고, 직렬화도 제공하지 않을 때 그렇다)
  • 2. 상속을 위한 클래스가 직렬화를 지원하지 않기로 했다면, 접근할 수 있는 기본 생성자를 제공하는 것이 좋다
직렬화를 지원하지 않으면서 상태를 가진 클래스
{code:java}
//==============================================
// AbstractFoo.java
//==============================================
import java.io.*;

public abstract class AbstractFoo{
private int x, y;
private boolean initialized = false;

public AbstractFoo(int x, int y){ initialize(x,y); }

protected AbstractFoo(){}

protected final void initialize(int x, int y){
if(initialized)
throw new IllegalStateException("Already initialized");

this.x = x;
this.y = y;

initialized = true;
}

protected final int getX(){ return x; }
protected final int getY(){ return y; }

private void checkInit() throws IllegalStateException{
if(!initialized)
throw new IllegalStateException("Uninitialized");
}

public String toString() {
return "x = "x", y = "+y;
}
}

{code:java}
//==============================================
// Foo.java
//==============================================
import java.io.*;

public class Foo  extends AbstractFoo implements Serializable{
	private void readObject(ObjectInputStream s)
		throws IOException, ClassNotFoundException{
		
		System.out.println("readObject ");
		
		s.defaultReadObject();
		
		int x = s.readInt();
		int y = s.readInt();
		initialize(x,y);

		
	}
	
	private void writeObject(ObjectOutputStream s)
		throws IOException {
		
		System.out.println("writeObject ");
		System.out.println(" x : "+ getX());
		System.out.println(" y : "+ getY());
		
		s.defaultWriteObject();
		
		s.writeInt(getX());
		s.writeInt(getY());		
	}


	
	public Foo(int x, int y){ super(x,y);}
}


|


//==============================================
// Main.java
//==============================================
import java.io.*;
import java.util.*;

public class Main{

	public static void main(String[] args){
		
		try{
		
		Foo fo = new Foo(10,4);		
				
		FileOutputStream fos = new FileOutputStream("a.txt");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		
		oos.writeObject(fo);
		oos.close();
		
		FileInputStream fis = new FileInputStream("a.txt");
		ObjectInputStream ois = new ObjectInputStream(fis);
		
		Foo foCp = (Foo)ois.readObject();
		
		System.out.println("readObject : " + foCp);
		System.out.println(foCp.getX());
		System.out.println(foCp.getY());
		
		ois.close();
		
		}catch(Exception e){}
	}
}

|

10-2. 맞춤 직렬화 형태를 사용하라

  • 객체의 물리적 표현이 논리적 내용과 차이가 많이 날 때 맞춤 직렬화 형태를 상용한다.
바람직한 맞춤 직렬화 형태를 제공하는 StringList
{code:java}
//==============================================
// StringList.java
//==============================================
import java.io.*;

public class StringList implements Serializable{

public transient int size = 0;
public transient Entry head = null;

private static class Entry{
String data ="D";
Entry next;
Entry previous;
}

public void add(String s){

}

private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(size);

for(Entry e=head; e != null ; e = e.next)
s.writeObject(e.data);
}

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{
s.defaultReadObject();
int numElements = s.readInt();

for(int i = 0; i< numElements; i++){
add((String)s.readObject());
}
}
}

 | {code:java}
//==============================================
// Main.java
//==============================================
import java.io.*;
import java.util.*;

public class Main{

	public static void main(String[] args){
		
		try{

		StringList sl = new StringList();
		
		System.out.println("writeObject : " + sl );

		FileOutputStream fos = new FileOutputStream("a.txt");
		ObjectOutputStream oos = new ObjectOutputStream(fos );

		oos.writeObject(sl);
		oos.close();

		FileInputStream fis = new FileInputStream("a.txt");
		ObjectInputStream ois = new ObjectInputStream(fis);
		
		StringList  slcp = (StringList  )ois.readObject();

		System.out.println("readObject : " + slcp );
		
		ois.close();

		}catch(Exception e){}
	}
}

|

10-3. readObject 메소드는 모든 공격을 방어할 수 있도록 작성하라

  • 1. readObject 메소스가 또 하나의 public 생성자나 마찬가지기 때문에 기본 직렬화 형태를 쓰면 클래스의 불변규칙이 깨질수 있다
{code:java}
//==============================================
// Period.java
//==============================================
import java.io.*;
import java.util.*;

public final class Period implements Serializable{
//private final Date start;
//private final Date end;
private Date start;
private Date end;

public Period(Date start, Date end){
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());

if(this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(start + " > " + end);
}

public Date start(){ return (Date) start.clone();}
public Date end(){ return (Date)end.clone();}

public String toString(){ return start + "-" + end; }

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException
{
s.defaultReadObject();

start = new Date(start .getTime());
end = new Date(end.getTime());

if(start.compareTo(end) > 0)
throw new InvalidObjectException(start + " after " + end);
}
}

{code:java}
//==============================================
// MutablePeriod.java
//==============================================
import java.io.*;
import java.util.*;

public class MutablePeriod{

	public final Period period;
	
	public final Date start;
	
	public final Date end;
	
	public MutablePeriod(){
		try{
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			
			ObjectOutputStream out = new ObjectOutputStream(bos);
			
			out.writeObject(new Period(new Date(),new Date()));
			
			byte[] ref = {0x71,0,0x7e,0,5};
			bos.write(ref);
			ref[4] = 4;
			bos.write(ref);
			
  ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
			period = (Period) in.readObject();
			
			start = (Date) in.readObject();
			end = (Date) in.readObject();
		}catch(Exception e){
			throw new RuntimeException(e.toString());
		}
	}
}



|


//==============================================
// Main.java
//==============================================
import java.io.*;
import java.util.*;

public class Main{

	public static void main(String[] args){
		try{

		MutablePeriod mp = new MutablePeriod();
		Period p = mp.period;
		Date pEnd = mp.end;
		
		pEnd.setYear(78);
		System.out.println(p);

		pEnd.setYear(69);
		System.out.println(p);

		}catch(Exception e){}
	}
}

|

10-4. 필요하다면 readResolve 메소드를 제공하라.

  • 1. 클래스의 선언부에 implements Serializable 을 붙이는 순간 싱글톤을 유지하기 어렵다.
    (역직렬화하면 언제나 다른 새로운 인스턴스가 또 하나 생성된다)
바람직한 맞춤 직렬화 형태를 제공하는 StringList
{code:java}
//==============================================
// Elvis.java
//==============================================
import java.io.*;

public class Elvis implements Serializable{
public static final Elvis INSTANCE = new Elvis();
transient String name = "Elvis";

private Elvis(){
System.out.println("This is the Elvis Class");
}

public static Elvis getInstance(){
return INSTANCE;
}

private Object readResolve() throws ObjectStreamException
{
return INSTANCE;
}

public String getName(){
return name;
}
}

 | {code:java}
//==============================================
// Main.java
//==============================================
import java.io.*;

public class Main{
	void writeObj(){
		Elvis e = Elvis.getInstance();
		try{
			FileOutputStream fos = new FileOutputStream("a.txt");
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(e);
			
			
		}catch(Exception ex){}
	}
	
	Elvis readObj() throws Exception
	{
		Elvis e = null;
		try{
			FileInputStream fis = new FileInputStream("a.txt");
			ObjectInputStream ois = new ObjectInputStream(fis);
			e=(Elvis)ois.readObject();
		}catch(Exception ex){}
		return e;
	}
	
	public static void main(String args[]){
		try{
			Main main = new Main();
			
			System.out.println(Elvis.getInstance());
			
			main.writeObj();
			
			System.out.println(main.readObj());
		}catch(Exception e){}
	}
}

|