직렬화
컴퓨터에 저장했다가 다음에 다시 꺼내 쓸 수는 없을지 또는 네트웍을 통해 컴퓨터 간에 서로 객체를 주고 받을 수는 없을까라고 고민해 본 적이 있는가? 과연 이러한 일들이 가능할까?
가능하다. 이러한 것을 직렬화가 처리해준다.
1. 직렬화란
직렬화(스트림으로)란 객체를 데이터 스트림으로 만드는 것을 뜻한다. 즉, 객체에 저장된 데이터를 스트림에 쓰기 위해 연속적인 데이터로 변환하는 것을 말한다.
반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화(객체로)라고 한다.
객체 스트림은 프로그램 메모리상에 존재하는 객체를 직접 입출력해 줄 수 있는 스트림으로 현재 상태를 보존하기 위한 영속성을 지원할 수 있다.
자바에서 객체 안에 저장되어 있는 내용을 파일로 저장하거나 네트워크를 통하여 다른 곳으로 전송하려면 객체를 바이트 형태로 일일이 분해해야 한다.
이를 위하여 객체를 직접 입출력 할 수 있도록 해주는 객체 스트림이다.
직렬화(Serialization)
- 객체를 데이터스트림(스트림에 쓰기(write)위한 연속적인(serial) 데이터)으로 만드는 것.
- 예) 객체를 컴퓨터에 저장했다가 꺼내 쓰기. 네트워크를 통한 컴퓨터 간의 객체 전송.
역직렬화(Deserialization)
- 스트림으로부터 데이터를 읽어서 객체를 만드는 것.
사실 객체를 저장하거나 전송하려면 당연히 직렬화를 거칠수 밖에 없다.
객체를 저장한다는 것이 무엇을 의미하는지 상기시켜야 한다.
객체는 클래스에 정의된 인스턴스변수의 집합이다. 객체에는 클래스변수나 메서드가 포함되지 않는다. 객체는 오직 인스턴스변수들로만 구성되어 있다.
인스턴스변수는 인스턴스마다 다른 값을 가질 수 있어야하기 때문에 별도의 메모리공간이 필요하지만 메서드는 변하는 것이 아니라서 메모리를 낭비해 가면서 인스턴스마다 같은 내용의 코드를 포함시킬 이유가 없다.
위의 그림은 6장에 나오는 Tv클래스의 객체가 생성되었을 때 사용한 그림인데, 왼쪽 그림은 이해를 돕기 위해 인스턴스에 메서드를 포함시켜서 그렸지만, 실제로는 오른쪽 그림과 같이 인스턴스에 메서드가 포함되지 않는것이 더 정확한 그림이다. 그래서 객체를 저장한다는 것은 바로 객체의 모든 인스턴스변수의 값을 저장한다는 것과 같은 의미이다. 어떤 객체를 저장하고자 한다면, 현재 객체의 모든 인스턴스변수의 값을 저장하기만 하면된다. 그리고 저장했던 객체를 다시 생성하려면, 객체를 생성한 후에 저장했던 값을 읽어서 생성한 객체의 인스턴스변수에 저장하면 되는 것이다.
클래스에 정의된 인스턴스변수가 단순히 기본형일 때는 인스턴스변수의 값을 저장하는 일이 간단하지만. 인스턴스변수의 타입이 참조형일 때는 그리 간단하지 않다. 예를 들어 인스턴스변수의 타입이 배열이라면 배열에 저장된 값들도 모두 저장되어야 할 것이다. 그러나 우리는 객체를 어떻게 직렬화해야 하는지 전혀 고민하지 않아도 된다. 다만 객체를 직렬화/역직렬화할 수 있는 ObjectInputStream과 ObjectOutputStream을 사용하는 방법만 알면 된다.
--> 두 객체가 동일한지 판단하는 기준이 두 객체의 인스턴스변수 값들이 같고 다름이라는 것을 기억하자.
2. ObjectInputStream(직렬화) / ObjectOutputStream(역직렬화)
직렬화(스트림에 객체를 출력)에는 ObjectInputStream을 사용하고 역직렬화(스트림으로부터 객체를 입력)에는 ObjectOutputStream을 사용한다.
ObjectInputStream과 ObjectOutputStream은 각각 InputStream / OutputStream을 직접 상속받지만 기반스트림을 필요로하는 보조스트림이다. 그래서 객체를 생성할 때 입출력(직렬화/역직렬화)할 스트림을 지정해주어야 한다.
만일 파일에 객체를 저장(직렬화)하고 싶다면 다음과 같이 해야 한다.
※ UserInfo 객체를 직렬화하여 저장
FileOutputStream fos = new FileOutputStream("objectfile.ser");
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(new UserInfo());
--> objectfile.ser이라는 파일에 UserInfo객체를 직렬화하여 저장한다. 출력할 스트림(FileOutputStream)을 생성해서 이를 기반 스트림으로 하는 ObjectOutputStream을 생성한다.
ObjectOutputStream의 writeObject(Object obj)를 사용해서 객체를 출력하면, 객체가 파일에 직렬화되어 저장된다.
※ UserInfo 객체를 역직렬화
FileInputStream fis = new FileInputStream("objectfile.ser");
ObjectInputStream in = new ObjectInputStream(fis);
UserInfo Info = (UserInfo)in.readObject();
--> 직렬화할 때와는 달리 입력스트림을 사용하고 writeObject(Object obj)대신 readObject()를 사용하여 저장된 데이터를 읽기만 하면 객체로 역직렬화된다.
다만, readObject()의 반환타입이 Object이기 때문에 객체 원래의 타입으로 형변환 해주어야 한다.
(1) 객체 전송의 단계
객체를 분해하여 전송하기 위해서는 직렬화(Serialization) 되어야 한다.
객체를 전송하기 위해서는 3가지 단계를 거친다.
1) 직렬화된 객체를 바이트 단위로 분해한다. (marshalling)
2) 직렬화 되어 분해된 데이터를 순서에 따라 전송한다.
3) 전송 받은 데이터를 원래대로 복구한다. (unmarshalling)
(2) 마샬링 (marshalling)
마샬링(marshalling)은 데이터를 바이트의 덩어리로 만들어 스트림에 보낼 수 있는 형태로 바꾸는 변환 작업을 뜻한다.
자바에서 마샬링을 적용할 수 있는 데이터는 원시 자료형(boolean, char, byte, short, int, long, float, double)와 객체 중에서 Serializable 인터페이스를 구현한 클래스로 만들어진 객체이다. 객체는 원시 자료형과 달리 일정한 크기를 가지지 않고 객체 내부의 멤버 변수가 다르기 때문에 크기가 천차만별로 달라진다. 이런 문제점을 처리할 수 있는게 ObjectOutputStream 클래스이다.
(3) 직렬화 (Serializable)
마샬링으로 바이트로 분해된 객체는 스트림을 통해서 나갈 수 있는 준비가 되었다. 앞에서 언급한대로 객체를 마샬링하기 위해서는 Serializable 인터페이스를 구현한 클래스로 만들어진 객체에 한해서만 마샬링이 진행될 수 있다. Serializable 인터페이스는 아무런 메소드가 없고 단순히 자바 버추얼 머신에게 정보를 전달하는 의미만을 가진다.
* 직렬화가 가능한 객체의 조건
1) 기본형 타입(boolean, char, byte, short, int, long, float, double)은 직렬화가 가능
2) Serializable 인터페이스를 구현한 객체여야 한다. (Vector 클래스는 Serializable 인터페이스구현)
3) 해당 객체의 멤버들 중에 Serializable 인터페이스가 구현되지 않은게 존재하면 안된다.
4) transient 가 사용된 멤버는 전송되지 않는다. (보안 변수 : null 전송)
(4) 언마샬링 (unmarshalling)
언마샬링은 객체 스트림을 통해서 전달된 바이트 덩어리를 원래의 객체로 복구하는 작업이다. 이 작업을 제대로 수행하기 위해서는 반드시 어떤 객체 형태로 복구할지 형 변환을 정확하게 해주어야 한다.
Vector v = (Vector)ois.readObject();
// OutputInputStream의 객체를 읽어서 Vector 형으로 형변환 한다.
이때 ObjectInputStream을 사용하여 데이터를 복구한다.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class ObjectStream {
public static void main(String[] args){
// ObjectOutputStream 을 이용한 객체 파일 저장
FileOutputStream fos = null;
ObjectOutputStream oos = null;
// UserClass 에 이름과 나이를 입력하여 객체를 3개 생성한다.
UserClass us1 = new UserClass("하이언", 30);
UserClass us2 = new UserClass("스티브", 33);
UserClass us3 = new UserClass("제이슨", 27);
try{
// object.dat 파일의 객체 아웃풋스트림을 생성한다.
fos = new FileOutputStream("object.dat");
oos = new ObjectOutputStream(fos);
// 해당 파일에 3개의 객체를 순차적으로 쓴다
oos.writeObject(us1);
oos.writeObject(us2);
oos.writeObject(us3);
// object.dat 파일에 3개의 객체 쓰기 완료.
System.out.println("객체를 저장했습니다.");
}catch(Exception e){
e.printStackTrace();
}finally{
// 스트림을 닫아준다.
if(fos != null) try{fos.close();}catch(IOException e){}
if(oos != null) try{oos.close();}catch(IOException e){}
}
// 파일로 부터 객체 데이터 읽어온다.
FileInputStream fis = null;
ObjectInputStream ois = null;
try{
// object.dat 파일로 부터 객체를 읽어오는 스트림을 생성한다.
fis = new FileInputStream("object.dat");
ois = new ObjectInputStream(fis);
// ObjectInputStream으로 부터 객체 하나씩 읽어서 출력한다.
// (UserClass) 로 형변환을 작성해야 한다.
// System.out.println 으로 객체의 구현된 toString() 함수를 호출한다.
System.out.println( (UserClass)ois.readObject());
System.out.println( (UserClass)ois.readObject());
System.out.println( (UserClass)ois.readObject());
}catch(Exception e){
e.printStackTrace();
}finally{
// 스트림을 닫아준다.
if(fis != null) try{fis.close();}catch(IOException e){}
if(ois != null) try{ois.close();}catch(IOException e){}
}
}
}
실행결과)
- UserClass 객체를 생성하여 ObjectOutputStream을 통해 object.dat 에 순차적으로 객체를 쓴다.
- UserClass 객체를 Stream에 쓰기위해서는 Serializable 인터페이스를 사용해야 직렬화 할 수 있다.
- Serializable 구현하지 않으면 NotSerializableException이 발생한다.
- ObjectInputStream을 통해 object.dat에 저장되어 있는 객체를 읽어온다.
- ObjectInputStream에서 readObject()로 읽을때는 정확한 형변환을 해주어야 정확하게 언마샬링을 할 수 있다.
3. Serializable 과 transient
(1) Serializable
데이터를 파일에 쓰거나, 네트워크를 타고 다른 곳에 전송할 때는 데이터를 바이트 단위로 분해하여 순차적으로 보내야 한다. 이것을 직렬화(Serialization)라고 한다.
기본 자료형(boolean, char, byte, short, int ,long, float, double)은 정해진 바이트의 변수이기 때문에 바이트 단위로 분해하여 전송한 후 다시 조립하는데 문제가 없다.
하지만 객체의 크기는 가변적이며, 객체를 구성하는 자료형들의 종류와 수에 따라 객체의 크기는 다양하게 바뀔 수 있다. 이런 객체를 직렬화 하기 위해서 Serializable 인터페이스를 구현하게 된다.
* 직렬화가 가능한 객체의 조건
① 기본형 타입(boolean, char, byte, short, int, long, float, double)은 직렬화가 가능
② Serializable 인터페이스를 구현한 객체여야 한다. (Vector 클래스는 Serializable 인터페이스구현)
③ 해당 객체의 멤버들 중에 Serializable 인터페이스가 구현되지 않은게 존재하면 안된다.
④ transient 가 사용된 멤버는 전송되지 않는다. (보안 변수 : null 전송)
객체 직렬화는 객체에 implements Serializable 만 선언해 주면 된다.
(2) transient
하지만, 객체의 데이터 중 일부의 데이터는(패스워드와 같은 보안) 여러가지 이유로 전송을 하고 싶지 않을 수 있다. 이러한 변수는 직렬화에서 제외해야 되며, 이를 위해서 변수에 transient를 선언한다.
또한, 직렬화 조건 중 객체의 멤버들 중에 Serializable 인터페이스 구현되지 않은 객체가 있으면, 직렬화 할 수 없다.(NonSerializableException) 직렬화 해야 되는 객체 안의 객체 중 Serializable 인터페이스가 구현되지 않으면서 전송하지 않아도 되는 객체 앞에는 transient 를 선언해준다. 그러면 직렬화 대상에서 제외되므로 해당 객체는 직렬화가 가능해진다.
public class UserInfo implements Serializable{
String name;
String password;
int age;
Object ob = new Object();
// 모든 클래스의 최고조상인 Object는 Serializable을
// 구현하지 않았기 때문에 직렬화가 불가능하다.
Object obj = new String("abc"); // String은 직렬화될 수 있다.
// 직렬화 제외
transient String weight;
transient Object obe = new Object();
}
cp.) 직렬화하고자 하는 객체의 클래스에 직렬화가 안 되는 객체에 대한 참조를 포함하고 있다면, 제어자 transient를 붙여서 직렬화 대상에서 제외되도록 할 수 있다.
또는 password와 같이 보안상 직렬화되면 안되는 값에 대해서 transient를 사용할 수 있다.
다르게 표현하면 transien가 붙은 인스턴스변수의 값은 그 타입의 기본값으로 직렬화된다고 볼 수 있다.
즉, UserInfo객체를 역직렬화하면 참조변수인 obj와 password의 값은 null이 된다.
※ Serializable 과 transient 사용 예제
UserClass.java
import java.io.Serializable;
// 직렬화 한다.
public class UserClass implements Serializable{
private static final long serialVersionUID = 4220461820168818967L;
String name;
// age 비 전송
transient int age;
// NonSerializable 클래스
NonSerializableClass nonSerializable;
public UserClass() {
}
public UserClass(String name, int age){
this.name = name;
this.age = age;
this.nonSerializable = new NonSerializableClass(false);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public NonSerializableClass getNonSerializable() {
return nonSerializable;
}
public void setNonSerializable(NonSerializableClass nonSerializable) {
this.nonSerializable = nonSerializable;
}
@Override
public String toString() {
return "UserClass [name=" + name + ", age=" + age
+ ", nonSerializable=" + nonSerializable + "]";
}
}
public class NonSerializableClass {
boolean serializable;
public NonSerializableClass(){
this.serializable = false;
}
public NonSerializableClass(boolean serializable){
this.serializable = serializable;
}
}
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class ObjectStream {
public static void main(String[] args){
// ObjectOutputStream 을 이용한 객체 파일 저장
FileOutputStream fos = null;
ObjectOutputStream oos = null;
// UserClass 에 이름과 나이를 입력하여 객체를 3개 생성한다.
UserClass us1 = new UserClass("하이언", 30);
UserClass us2 = new UserClass("스티브", 33);
UserClass us3 = new UserClass("제이슨", 27);
try{
// object.dat 파일의 객체 아웃풋스트림을 생성한다.
fos = new FileOutputStream("object.dat");
oos = new ObjectOutputStream(fos);
// 해당 파일에 3개의 객체를 순차적으로 쓴다
oos.writeObject(us1);
oos.writeObject(us2);
oos.writeObject(us3);
// object.dat 파일에 3개의 객체 쓰기 완료.
System.out.println("객체를 저장했습니다.");
}catch(Exception e){
e.printStackTrace();
}finally{
// 스트림을 닫아준다.
if(fos != null) try{fos.close();}catch(IOException e){}
if(oos != null) try{oos.close();}catch(IOException e){}
}
// 파일로 부터 객체 데이터 읽어온다.
FileInputStream fis = null;
ObjectInputStream ois = null;
try{
// object.dat 파일로 부터 객체를 읽어오는 스트림을 생성한다.
fis = new FileInputStream("object.dat");
ois = new ObjectInputStream(fis);
// ObjectInputStream으로 부터 객체 하나씩 읽어서 출력한다.
// (UserClass) 로 형변환을 작성해야 한다.
// System.out.println 으로 객체의 구현된 toString() 함수를 호출한다.
System.out.println( (UserClass)ois.readObject());
System.out.println( (UserClass)ois.readObject());
System.out.println( (UserClass)ois.readObject());
}catch(Exception e){
e.printStackTrace();
}finally{
// 스트림을 닫아준다.
if(fis != null) try{fis.close();}catch(IOException e){}
if(ois != null) try{ois.close();}catch(IOException e){}
}
}
}
UserClass.java 의 변수를 보면 transient int age; 로 age 변수는 직렬화에서 제외했다.
- NonSerializableClass 객체는 Serializable 인터페이스를 구현하지 않은 클래스이다.
- 따라서 UserClass.java 로 직렬화를 시도하면, 위와 같이 NonSerializableClass Exception이 발생한다.
- 위의 문제를 해결하기 위해서는 NonSerializableClass.java 에 Serializable 인터페이스를 구현하여 직렬화를 할 수 있게 하는 방법과
- NonSerializableClass 를 전송하지 않아도 되면, 또는 않아야 한다면 transient 를 앞에 붙여주는 것이다.
- 그러면 NonSerializableClass 객체는 직렬화 대상에서 제외되면서 UserClass 가 정상적으로 직렬화되어 처리될 것이다.
※ NonSerializableClass 객체 선언 앞에 transient 선언 결과
import java.io.Serializable;
// 직렬화 한다.
public class UserClass implements Serializable{
private static final long serialVersionUID = 4220461820168818967L;
String name;
// age 비 전송
transient int age;
// NonSerializable 클래스
transient NonSerializableClass nonSerializable;
public UserClass() {
}
public UserClass(String name, int age){
this.name = name;
this.age = age;
this.nonSerializable = new NonSerializableClass(false);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public NonSerializableClass getNonSerializable() {
return nonSerializable;
}
public void setNonSerializable(NonSerializableClass nonSerializable) {
this.nonSerializable = nonSerializable;
}
@Override
public String toString() {
return "UserClass [name=" + name + ", age=" + age
+ ", nonSerializable=" + nonSerializable + "]";
}
}
- 객체가 정상적으로 직렬화되어 전송되고, 가져와 출력되는 것을 볼 수 있다.
- 당연히 transient가 붙은 age 변수와 nonSerializable 은 직렬화 되지 않기에 데이터가 없다.
cp.) erialVersionUID 이란? Warning 해결하기
http://hyeonstorage.tistory.com/attachment/cfile26.uf@25748E385325AEA31EC4FD.zip
Add SerialVersionUID를 클릭하면 serialVersionUID가 생성된다.
'Programing > Java' 카테고리의 다른 글
[Java] 쓰레드의 우선순위 (0) | 2014.12.21 |
---|---|
[Java] 쓰레드 기본 (0) | 2014.12.21 |
[Java] File 클래스 (0) | 2014.12.16 |
[Java] 문자 기반 스트림 (0) | 2014.12.16 |
[Java] 바이트 기반의 스트림 (0) | 2014.12.16 |