Dev/Java

직렬화(Serialization) - 객체를 주고 받는 방법

린네의 2024. 4. 15. 22:02

 

 

예제 소스 링크

 

직렬화란?

 

객체를 데이터 스트림으로 만드는 것을 뜻한다. 다시 얘기하면 객체에 저장된 데이터를 스트림에 쓰기 위해 연속적인 데이터로 변환하는 것을 의미한다. 반대로 스트림으로부터 데이터를 읽어와서 객체를 만드는 것을 역직렬화(deserialization)라고 한다. 

 

객체는 클래스에 정의된 인스턴스변수의 집합이다. 객체에는 클래스변수나 메서드가 포함되지 않는다. 객체는 오직 인스턴스변수들로 구성되어 있다.

 

인스턴스변수는 인스턴스마다 다른 값을 가질 수 있어야하기 때문에 별도의 메모리 공간이 필요하지만, 메서드는 변하는 것이 아니라서 메모리를 낭비해 가면서 인스턴스마다 같은 내용의 코드(메서드)를 포함시킬 이유는 없다.

 

그래서 객체를 저장한다는 것은 바로 객체의 모든 인스턴스변수의 값을 저장한다는 것과 같은 의미이다. 어떤 객체를 저장하고자 한다면, 현재 객체의 모든 인스턴스변수의 값을 저장하기만 하면 된다.  인스턴스변수의 타입이 참조형일 때는 인스턴스변수의 값을 저장하는 것이 까다로울 수 있는데, 이럴 때 사용하는 것이 ObjectInputStream과 ObjectOuputStream이다.

 

 

ObjectInputStream과 ObjectOutputStream

직렬화에는 ObjectOutputStream을 사용하고 역직렬화에는 ObjectInputStream을 사용한다. Ouput은 스트림에 객체를 출력할 때, Input은 스트림으로부터 객체를 받을 때 사용한다고 생각하면 된다. 

 

ObjectOutputStream과 ObjectInputStream은  각각 OutputStream과 InputStream을 상속받는 보조스트림으로, 객체를 생성할 때 입출력할 스트림을 지정해줘야 한다.

 

다음은 objectfile.ser이라는 파일을 UserInfo 객체를 직렬화하여 저장하는 예제이다.

FileOutputStream fos = new FileOutputStream("objectfile.ser");
ObjectOutputStream out = new ObjectOutputStream(fos);

out.writeObject(new UserInfo());

 

역직렬화 예제도 보자.

FileInputStream fis = new FileInputStream("objectfile.ser");
ObjectInputStream in= new ObjectInputStream(fis);

UserInfo info = (UserInfo)in.readObject();

 

이 외에 에도 다양한 입출력 메서드를 제공한다.

 

 

객체를 직렬화/역직렬화하는 것은 객체의 모든 인스턴스변수가 참조하고 있는 모든 객체에 대한 것이기 때문에 상당히 복잡하며 시간도 오래 걸린다. readObject()와 writeObject()를 통한 자동 직렬화가 편리하기는 하지만 직렬화 작업시간을 단축시키려면 직렬화하고자 하는 객체의 클래스에 추가적으로 다음 두 개의 메서드를 직접 구현해주어야 한다.

private void writeObject(ObjectOutputStream out)
	throws IOException {
    //write메서드를 사용해서 직렬화를 수행한다
  }
  
private void readObject(ObjectInputStream in) 
	throws IOException {
    // read메서드를 사용해서 역직렬화를 수행한다.
 }

 

 

Serializable, transient를 사용해서 직렬화가 가능한 클래스를 만들어 보자 

직렬화하고자 하는 클래스가 java.io.Serializable 인터페이스를 구현하도록 하면 된다.  

public class Userinfo implements java.io.Serializable {
	String name;
	String password;
	int age;
}

 

 

Serializable 은 아무것도 없는 빈 인터페이스지만, 직렬화를 고려해서 작성한 클래스인지 판단하 t 기준이 된다.

 

여기서 주의할 점이, 모든 클래스의 최고조상인 Object는 Serialization변수를 구현하지 않았기 때문에 직렬화할 수 없다. ( 당연한 얘기지만, Object가 만약 Serialization을 구현했다면 모든 클래스가 직렬화될 수 있었을 것이다! ) 

 

public class UserInfo implements Serialization {
    String name;
    String password;
    String age;
    Object object = new Object(); // java.io.NotSerializableException
}

 

인스턴스변수의 타입이 아닌 실제로 연결된 객체의 종류에 의해서 결정되기 때문에 아래와 같은 코드는 참조변수가 Object로 선언되어 있지만 직렬화가가능하다.

 

public class UserInfo implements Serialization {
    String name;
    String password;
    String age;
    Object object = new String("abc");
}

 

 

  • transient 

직렬화하고자 하는 객체의 클래스에 직렬화가 안 되는 객체에 대한 참조를  포함하고 있다면 제어자 transient를 붙여서 직렬화 대상에서 제외될 수 있도록 할 수 있다. 반대로 말하면, 역직렬화 시 transient가 붙은 참조변수는 null 이 된다는 뜻이다.

 

public class Userinfo implements Serializable {
    String name;
    transient String password; // 직렬화 대상에서 제외
    int age;
    transient Objet obj = new Object(); // 직렬화 대상에서 제외 된다
}

 

 

직렬화와 역직렬화는 FileIntputStream, ObjectInputStream와 FileOutputStream, ObjectOutputStream을 쓴다는 점과 writeObject(), readObject()를 사용한다는 점을 빼면 거의 동일하다.

 

단, readObject()를 사용해서 역직렬화를 할 때 리턴타입이 Object 이므로 원래의 타입으로 형변환을 해줘야 하고 역직렬화 시 직렬화할 때의 순서와 일치해야 한다는 점에 유의하자.  이렇게 순서가 일치해야 하기 때문에, 직렬화할 객체가 많을 때는 각 객체를 개별적으로 직렬화하는 것보다 ArrayList와 같은 컬렉션에 저장해서 직렬화 하는 것이 좋다.

 

직렬화된 객체를 역직렬화할 때는 직렬화했을 때와 같은 클래스를 사용해야 한다. 그러나 클래스의 이름이 같더라도 클래스의 내용이 변경된 경우 역직렬화는 실패하며 다음과 같은 예외가 발생한다.

 

java.io.InvalidClassException: UserInfo; local class incompatible: stream classsdec serialVersionUID =.....

 

위 예외의 내용은 직렬화할 때와 역직렬화할 때의 클래스 버전이 같아야 하는데, 다르다는 것이다. 

객체가 직렬화될 때 클래스에 정의된 멤버들의 정보를 이용해서 serialVersionUID라는 클래스의 버전을 자동생성해서 직렬화 내용에 포함한다. 그래서 역직렬화할 때 클래스의 버전을 비교함으로써 직렬화할 때의 클래스 버전과 일치하는지 확인할 수 있는 것이다. 

 

static변수, 상수, transient가 붙은 인스턴스변수가 추가되는 경우에는 직렬화에 영향을 미치지 않기 때문에 클래스의 버전을 다르게 인식하도록 할 필요는 없다. 

 

네트워크로 객체를 직렬화하여 전송하는 경우, 보내는 쪽과 받는 쪽 모두 같은 버전의 클래스를 가지고 있어야 하는데 클래스가 조금만 변경되어도 해당 클래스를 재배포하는 것은 프로그램을 관리하게 어렵게 만든다. 

 

이럴 때는 클래스의 버전을 수동으로 관리하는 방법을 사용할 수 있다. 

class MyData implements java.io.Serializable {
    static final long serialVersionUID = 3432423L;
    int valuel;
}

 

위 예제는 serialVersionUID를 추가로 정의하여 클래스의 버전을 수동으로 관리한 코드이다.  serialVersionUID의 값을 정의해 주면 클래스의 내용이 바뀌어도 클래스의 버전이 자동생성된 값으로 변경되지 않는다. 이 값은 정수값이면 어떠한 값으로도 지정할 수 있지만, 서로 다른 클래스 간에 같은 값을 가지 않도록 serialver.exe를 사용해서 생성된 값을 사용하는 것이 보통이다.  serailver.exe 는 클래스에 serialVersionUID가 정의되어 있으면 그 값을 출력하고 정의되어 있지 않으면 자동 생성한 값을 출력한다.