[Java] 직렬화

2020. 2. 20. 16:53 JAVA/Java

직렬화


컴퓨터에 저장했다가 다음에 다시 꺼내 쓸 수는 없을지 또는 네트웍을 통해 컴퓨터 간에 서로 객체를 주고 받을 수는 없을까라고 고민해 본 적이 있는가? 과연 이러한 일들이 가능할까?

가능하다. 이러한 것을 직렬화가 처리해준다.


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 은 직렬화 되지 않기에 데이터가 없다.


4. 직렬화가능한 클래스의 버전관리
직렬화된 객체를 역직렬화할 때 서로 같은 클래스를 사용해야 하는데, 클래스의 이름이 같더라도 클래스의 내용이 변경된 경우 역직렬화가 실패하며 에러가 발생한다.
static 변수나 상수 또는 trasient가 붙은 인스턴스변수의 경우 직렬화에 영향을 미치지 않는다.

 java.io.InvalidClassException: UserInfo; local class incompatible: stream classdesc
 serialVersionUID = 6953673583338942489, local class serialVersion UID = -6256164443556992367 ...
 
해결책) serialVersionUID를 정의한다.
class MyData implements java.io.Serializable{
    static final long serialVersionUID = 3518731767529258119L;
    // 이렇게 추가해주면 클래스의 내용이 바뀌어도 클래스의 버전이 고정된다.
     
    int value;
}
 
serialVersionUID 얻기 (아무 정수를 써도 상관없지만 중복될 가능성때문에 사용하는 편이 좋다.)

cp.) erialVersionUID 이란? Warning 해결하기


객체를 파일에 쓰거나 전송하기 위해서는 직렬화를 해야 하는데 그러기 위해 객체 클래스에 Serializable 인터페이스를 implements 하게 된다.
하지만 Serializable 인터페이스를 implements 하게 되면 노란색 Warning이 발생한다.
The serializable class *** does not declare a static final serialVersionUID field of type long
저렇게 Warning이 발생하지만 동작하는데는 문제가 없다.
그래도 계속 저렇게 Warning이 떠있는데 왜 생기는 것이며 serialVersionUID 는 무엇이길래 없다고 그러는 건가?
serialVersionUID 는 직렬화에 사용되는 고유 아이디인데, 선언하지 않으면 JVM에서 디폴트로 자동 생성된다.
따라서 선언하지 않아도 동작하는데 문제는 없지만, 불안하기 때문에 JAVA에서는 명시적으로 serialVersionUID를 선언할 것을 적극 권장하고 있다.

* JVM에 의한 디폴트 serialVersionUID 계산은 클래스의 세부 사항을 매우 민감하게 반영하기 때문에 컴파일러 구현체에 따라서 달라질 수 있어 deserialization 과정에서 예상하지 못한 InvalidClassException을 유발할 수 있다.

serialVersionUID는 private static final 로 선언하면 된다.

그럼 serialVersionUID는 어떻게 생성하면 될까?
이클립스에서는 serialVersionUID를 자동으로 선언해주는 플러그인 있다.
위의 파일을 다운받고 압축을 풀어서 eclipse\plugin 폴더에 넣어 놓고 이클립스를 재시작 한다.

http://hyeonstorage.tistory.com/attachment/cfile26.uf@25748E385325AEA31EC4FD.zip


serialVersionUID 를 생성하고자 하는 (Serializable을 implements 한) 클래스에 마우스 오른쪽 버튼을 누르면 
아래 그림과 같이 Add SerialVersionUID 가 있다.


Add SerialVersionUID를 클릭하면 serialVersionUID가 생성된다.

앞에 private를 붙여서 private static final long 형태가 되도록 하자.


이제 노란 Warning이 없어지는 것을 볼 수 있다.
Warning을 없애는 방법은 SerialVersionUID 선언 외에 다른 방법이 있다.
클래스 위에 @SuppressWarnings("serial") 이라고 어노테이션 처리를 해주면 없어진다.
하지만 SerialVersionUID를 선언해주는 것이 권장되는 방법이다.



출처: https://devbox.tistory.com/entry/Java-직렬화?category=574549 [장인개발자를 꿈꾸는 :: 기록하는 공간]

'JAVA > Java' 카테고리의 다른 글

[Java] 데몬쓰레드  (0) 2020.02.20
[Java] 쓰레드 그룹  (0) 2020.02.20
[Java] 쓰레드의 우선순위  (0) 2020.02.20
[Java] 쓰레드 기본  (0) 2020.02.20
[Java] File 클래스  (0) 2020.02.20
[Java] 문자 기반 스트림  (0) 2020.02.20
[Java] 바이트 기반의 스트림  (0) 2020.02.18
[Java] 파일I/O 개요  (0) 2020.02.18