* 객체직렬화의 개념

 

자바 I/O 처리는 정수, 문자열, 바이트 단위의 처리만 지원했었다.

따라서 복잡한 객체의 내용을 저장/복원하거나, 네트워크로 전송하기 위해서는 객체의 멤버변수의

각 내용을 일정한 형식으로 만들어(이것을 패킷이라고 한다) 전송해야 했다.


객체직렬화는 객체의 내용(구체적으로는 멤버변수의 내용)을 자바 I/O가 자동적으로 바이트 단위로 변환하여, 저장/복원하거나 네트워크로 전송할 수 있도록 기능을 제공해준다.

즉, 개발자 입장에서는 객체가 아무리 복잡하더라도, 객체직렬화를 이용하면 객체의 내용을 자바 I/O가 자동으로 바이트 단위로 변환하여 저장이나 전송을 해주게 된다.

또한 이것은 자바에서 자동으로 처리해주는 것이기 때문에, 운영체제가 달라도 전혀 문제되지 않는다.

 

객체를 직렬화할때 객체의 멤버변수가 다른 객체(Serializable 인터페이스를 구현한)의 레퍼런스 변수인 경우에는 레퍼런스 변수가 가리키는 해당 객체까지도 같이 객체직렬화를 해버린다.

또 그 객체가 다른 객체를 다시 가리키고 있다면, 같은 식으로 객체직렬화가 계속해서 일어나게 된다.

이것은 마치 객체직렬화를 처음 시작한 객체를 중심으로 트리 구조의 객체직렬화가 연속적으로 일어나는 것이다.

 

 

* 도입이유

 

1. RMI의 도입

RMI는 원격객체통신을 지원해야 하기 때문에, 객체의 내용이 투명하게 이동할 수 있어야 한다

 

2. Beans

Beans는 설계시에 상태정보를 지정할 수 있는데, 이것을 마땅히 저장할 메커니즘이 없다.

이때 객체직렬화를 사용하면, 편하게 객체의 상태정보를 저장하는 것이 가능하다

 

3. 간단한 네트워크 프로그램이나 파일 프로그램은 객체직렬화를 사용하면, 코딩이 반으로 준다.

 

 

 

* 객체 직렬화의 과정


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

직렬화에 대한 간단한 예제이다.

ObjectOutputStream을 생성해서 writeObject() 메서드를 이용해서 객체를 직렬화하고,
ObjectInputStream을 생성해서 readObject() 메서드를 통해서 객체를 복원한다.
또한 SerializableClass가 Serializable을 implements한 것을 주의해서 보아야 한다.


----------------------------------------------------------------------------------

ObjectSerializableTest.java



import java.io.*;


public class ObjectSerializeTest {

public static void main(String[] args) throws Exception {


// 파일을 열어서 그곳에 객체를 직렬화시켜서 저장한다.
// 파일 출력스트림을 연다.
FileOutputStream fileout = new FileOutputStream("test.txt");


// 객체스트림을 열고, 객체스트림을 통해 객체를 파일에 저장
ObjectOutputStream out = new ObjectOutputStream(fileout);
out.writeObject(new SerializableClass("Serialize Test Program", 1014));


// 객체스트림을 닫는다.
out.close();


// 직렬화 된 객체를 저장된 파일로 부터 객체를 해제시켜 원래의 객체로 복원
// 파일 입력스트림으로부터 객체 입력스트림을 연다.
FileInputStream fileinput = new FileInputStream("test.txt");
ObjectInputStream in = new ObjectInputStream(fileinput);


// 객체 입력스트림으로부터 객체를 읽어온다.
SerializableClass sc = (SerializableClass)in.readObject();


// 객체스트림을 닫는다.
in.close();


// 스트림으로부터 읽어들인 객체의 내용을 출력
// 원래 생성되었던 객체와 같은 값을 갖는다는 것을 알수가 있다.
System.out.println("String : " + sc.Sstr);
System.out.println("Integer : " + sc.Sint);
}
}


// 하나의 문자열과 정수를 저장하고 있는 SerializableClass를
// Serializable을 implements 함으로써
// 스트림을 통해 직렬화되고 해제되어질 수 있다.

class SerializableClass implements Serializable {

public String Sstr;
public int Sint;


// 생성자
public SerializableClass(String s, int i) {
this.Sstr = s;
this.Sint = i;
}
}



-----------------------------------------------------------------
실행결과


String : Serialize Test Program
Integer : 1014





========================================================================================

자바는 객체지향언어이다.

즉, 가상머신 내에 존재하는 것은 모두 객체들로 이루어져 있습니다.

물론, 객체를 만들기 위한 클래스들도 있겠죠.

클래스의 형태를 보고 필요하다면 언제든지 객체를 만들어 낼 수 있습니다.

객체와 클래스의 관계를 논하지 않고서는 아무것도 할 수 없겠죠.

컴파일 한 후, 생성된 .class파일은 클래스의 모든 정보를 담고 있으며,

이 .class의 바이트는 가상머신에 Class클래스의 형태로 로딩 되어 집니다.

그리고, 로딩된 클래스의 정보를 보고 객체를 만들게 됩니다.

클래스는 데이터 타입이며 이 데이터타입이 있어야 그 모양을 보고 객체의 메모리를 생성할 수 있습니다.

 

특정 클래스의 객체를 만들었을 때 객체는 무엇으로 이루어져 있을까요?

객체는 멤버변수의 메모리의 크기와 같다.

객체는 멤버변수의 메모리만으로 이루어져 있습니다.

만약, 객체를 이용해서 메서드를 호출한다면 객체가 보유하고 있는 값과 호출할 메서드의 형태만 있으면 언제든지 호출 가능합니다.

메서드의 형태는 클래스의 정보가 있는 부분에 있을 것이고,

메서드 내에 멤버변수가 사용되어진다면, 그 값은 객체 내에 있으니 이 두 가지를 조합한다면 언제든지 메서드를 호출할 수 있습니다.

 

가상머신에 존재하는 객체메모리 그 자체를 저장하거나, 통째로 네트웍으로 전송하려고 합니다.

저장을 하든 네트웍으로 전송을 하든간에 객체는 일련의 바이트의 형태로 되어 있어야 합니다.

어떤 규칙에 의해서 객체 메모리를 한 줄로 늘어선 바이트의 형태로 만들고,

다시 객체의 형태로 복원하는 작업을 우리는 객체 직렬화라고 합니다

 

 

‘이름’, ‘부서’, ‘직책’ 이라는 속성을 가진 직원 클래스가 있고, 이 클래스를 이용하여 두 개의 객체 (직원1 객체와 직원2 객체)가 생성되어 메모리에 저장되어 있다면,

직원1 객체는 이름이 홍길동이고 부서는 총무부, 직책은 과장이라는 상태 정보를 저장하고 있습니다.

 

이러한 객체들이 저장 되어 있는 메모리는 휘발성이기 때문에,

컴퓨터의 전원을 종료하게 되면 객체의 상태 정보는 모두 사라집니다.

그래서 우리는 이 정보를 데이터베이스에 저장하거나 아니면 따로 기록해 두는 것입니다.

다시 객체로 만들려면 데이터베이스 내용을 검색해서 해당 내용을 찾아와서 객체의 형태로 다시 조합해야 합니다.

 

이러한 방법을 사용하는 대신, 객체 그 자체를 바로 저장하고 다시 불러왔을 때 원래의 객체 형태 그 자체라면 아주 효율적이겠죠.

 

정민철,영업부,부장 이라는 정보를 묶어서 소켓으로 전송한다면 다음과 같이 전송할 겁니다.

 

정민철|영업부|부장

 

이러한 방법으로 보내면 받는쪽에서는 “|”을 구분자로 해서 하나씩 분해해야 합니다.

만약 객체자체를 보낸다면 상황은 다릅니다. 다음과 같은 객체를 보낸다면

 

--------------------------------------------------------------

Employee.java

 

 

public class Employee {

   private String name;             // 이름

   private String dept;               // 부서

   private String duties;            // 직책

 

   public Employee (String name, String dept, String duties) {

       this.name = name;

       this.dept = dept;

       this.duties = duties;

   }// 생성자

 

public static void main(String[] args){

       Employee Emp = new Employee("조한서", "인사","차장");

     //Emp 자체를 네트웍으로 전송

   }

}

 

Emp 객체를 네트웍을 통해서 받았다면 바로 객체를 사용할 수 있다는 장점이 있습니다.

파싱할 필요도 없고, 특별한 작업 없이도 객체를 사용할 수 있는 방법론을 제공하는 것이 바로 객체 직렬화입니다.

이 개념이 RMI, Java Beans등의 핵심 기술이 됩니다.

 

객체 직렬화는 상당히 복잡한 과정을 필요로 하지만, 내부적으로 완벽하게 감추어져 있기 때문에 객체 직렬화를 직접 구현을 하는 것이 아니라 규칙에 맞게 사용하는 방법을 배우는 것이라고 보면 됩니다.

 

객체 직렬화에 대해서 정리하면

 

객체 직렬화는 객체의 상태를 보존하는 방법론을 제공한다.

파일 스트림, 네트워크 스트림 등과 함께 사용할 수 있는 확장성을 제공한다.

 

실제로 객체 직렬화 기술은 원격메소드호출(RMI : Remote Method Invocation)과 같은 기술에서 중요하게 사용이 됩니다.

RMI는 한쪽의 자바 가상 머신 내에 있는 객체가 멀리 떨어져 있는 원격지의 자바 가상 머신 내 객체를 네트워크를 통해 접근하고,

메서드를 호출 할 수 있게 해주는 기술입니다.

두 자바 가상 머신 사이에 연결된 바이트 스트림을 통해 메서드의 인자나 반환 값으로 객체를 주고 받기 위해서 객체 직렬화 기술이 사용됩니다.

또한, EJB라는 기술에서도 EJB 컨테이너의 성능 향상을 위해 사용됩니다.

 

객체 스트림에 저장될 객체는 Serializable이나 Externalizable 인터페이스를 구현 함으로써, 객체 자신이 저장될 의사가 있음을 반드시 밝혀야 합니다.

 

Serializable인터페이스는 아무 메서드도 가지고 있지않은 표시(태그) 인터페이스입니다. Serializable인터페이스를 구현한 객체는 객체 스트림 클래스들이 자동으로 필드들의 값을 저장하고 복구 해주지만,

Externalizable인터페이스를 구현한 객체는 객체를 표현할 필드들의 종류와 값을 writeExternal()메서드와 readExternal()메서드를 사용해서 직접 저장하고 복원하는 과정을 구현해야 합니다. 

 

 

* interface Serializable

 

객체직렬화가 필요한 객체는 반드시 Serializable 인터페이스르 구현해야 한다.

그러나, Serializable 인터페이스는 객체가 직렬화가 제공되어야 함을 자바가상머신(JVM)에 알려주는 역할만을 하는 인터페이스다.

따라서, Serializable 인터페이스를 지정하였다고 해도, 구현할 메서드는 없다.

 

보낼 객체가 직렬화 되어 있으면 전송은 특정 장치에 연결되어 있는 스트림이 모두 해결합니다.

우선 파일이나 네트웍에 스트림을 생성 한 후 객체를 보낼 수 있는 스트림으로 변환을 합니다.

그리고 직렬화되어 있는 객체를 보내면 됩니다.

그 순서를 정리하면 다음과 같습니다.

 

1. 네트웍이나 파일등에 스트림을 생성한다.

2. 생성된 스트림을 Object스트림으로 변환한다.

3. 입력과 출력스트림은 ObjectInputStream과 ObjectOutputStream이다.

4. 직렬화된 객체를 객체스트림을 통해서 전송한다.

    - ObjectOutputStream à writeObject(직렬화된객체)

5. 객체 스트림을 통해서 직렬화된 객체를 받는다.

    - ObjectInputStream à readObject()

 

이와 같은 순서로 객체 직렬화를 구현합니다.

스트림은 I/O에서 제공해 주기 때문에 보낼 객체만 생각하면 됩니다.

보낼 객체에 impelements Serializable만 붙이면 됩니다.

 

-------------------------------------------------------------

SerializableObject.java

 

 

import java.io.*;

 

public class SerialObject implements Serializable{

   private String name;        // 이름

   private String dept;          // 부서

   private String duties;       // 직책

   public SerialObject (String name, String dept, String duties) {

       this.name = name;

       this.dept = dept;

       this.duties = duties;

   }

   public String toString(){

       return name + ":" + dept + ":" + duties;

   }

}

 

임의의 파일에 객체 스트림을 연결하여 객체를 읽고 기록해 봅니다.

아래의 예는 객체를 기록한 후 다시 읽어내는 예제입니다.

 

----------------------------------------------------------------------

SerialObject.java

 

 

import java.io.*;

 

public class SerialObjectTest {

      

   public static void main(String[] args) throws Exception {

 

     FileOutputStream fileout = new FileOutputStream("test.txt");

      ObjectOutputStream out = new ObjectOutputStream(fileout);

       SerialObject se1 = new SerialObject("김언어", "개발부", "팀장");

       SerialObject se2 = new SerialObject("김서리", "자금부", "부장");

       SerialObject se3 = new SerialObject("이회계", "경리부", "차장");

       out.writeObject(se1);

       out.writeObject(se2);

       out.writeObject(se3);

       out.close();

 

       FileInputStream filein = new FileInputStream("test.txt");

       ObjectInputStream in = new ObjectInputStream(filein);

       SerialObject iso1 = (SerialObject)in.readObject();

       SerialObject iso2 = (SerialObject)in.readObject();

       SerialObject iso3 = (SerialObject)in.readObject();

       System.out.println(iso1.toString());

       System.out.println(iso2.toString());

       System.out.println(iso3.toString9));

       in.close();

   }

}

 

 

test.txt파일에 파일출력스트림을 생성합니다.

그리고 이 파일출력스트림을 Object출력스트림으로 변환합니다.

스트림을 열었다면 implements Serializable로 구현된 객체를 만들어야합니다.

위의 예제에서는 3개의 객체를 만들었습니다

그리고 이 객체를 test.txt파일에 객체가 3개이니 3번 기록 해야 합니다.

마지막으로 출력스트림을 닫습니다.

소스의 이 부분까지 수행되면 test.txt가 만들어지고 객체 3개가 순서대로 기록되게 됩니다.

 

입력된 객체를 읽어 내기 위해서 test.txt파일에 파일입력스트림을 생성합니다.

그리고 생성된 파일입력스트림을 Object입력스트림으로 변환합니다.

변환된 Object입력스트림으로 객체를 읽어냅니다.

앞에서 3개 입력했으니 3번만 읽어 내도록 합니다.

그리고 Object입력스트림으로 읽었을 때

반환형이 Object형이기 때문에 강제 Downcasting시켜야 합니다.

마지막으로 Object입력스트림을 닫으시면 모든 작업은 끝납니다.

 

 

Object스트림은 스트림의 한 종류입니다.

직렬화 된 객체를 보낼 수 있는 스트림이라고 말할 수 있습니다. 

객체를 객체출력스트림에 쓸 때는 ObjectOutputStream 클래스의 writeObject() 메서드를 사용합니다.

writeObject()의 원형의 다음과 같습니다.

 

public final void writeObject(Object obj) throws IOException

 

 writeObject()메서드는 인자로 넘어 온 객체가 Serializable인터페이스나 Externalizable 인터페이스를 구현했는지 검사합니다.

 

주어진 객체가 Serializable 인터페이스를 구현했다면,

writeObject()메서드는 자동으로 객체의 상태를 스트림에 기록해 줍니다.

만약 객체가 Serializable이나 Externalizable인터페이스 중 어느것도 구현하지 않았다면, NotSerializableException을 발생시킵니다.

 

스트림에 직렬화 되어있는 객체는 ObjectInputStream 클래스의 readObject() 메서드를 사용해서 복원 할 수 있습니다.

readObject()의 원형은 다음과 같습니다.

 

public final Object readObject() throws IOException, ClassNotFoundException

 

 readObject()메서드는 연결된 스트림으로부터 객체의 상태 정보를 읽어 내고,

writeObject()메소드와 마찬가지로 readObject()메서드 역시 객체가 Serializable 인터페이스를 구현했다면 스트림에 쓰여져 있던 객체의 상태 정보를 기반으로 자동으로 새로운 객체를 복원해 줍니다.

 

 

* transient 키워드

 

객체직렬화 전후에 보존하고 싶지 않은 멤버변수가 있을 경우에는, 해당 멤버변수에 transient 키워드를 사용함으로써 객체직렬화시 내용을 저장하지 않을 수 있다.

예를 들어 패스워드나 중요한 정보는 객체직렬화로 저장을 하게되면, 복원시에 누구라도 내용을 도로 알아낼 수가 있다.

따라서, 이러한 정보를 갖게되는 멤버변수는 transient 키워드를 사용해서 선언해주는 것이 좋다.

 

스트림을 이용해서 직렬화 할 때 객체의 모든 상태정보를 직렬화하게 됩니다.

하지만, 클래스를 디자인하다 보면 순간적으로 사용하고 버리는 필요없는 정보도 있습니다. 이러한 정보를 제외 시키기 위해서 transient키워드를 사용합니다.

 

클래스를 만들다 보면 중요하지는 않지만 전역 변수로 사용하기 위해서 어쩔 수 없이 멤버변수로 만드는 경우가 있습니다.

저장할 필요가 없다고 생각된다면 접근지정자 다음에 transient를 붙이면 직렬화에서

제외되어 버립니다.

 

객체를 직렬화 하는데 제외하겠다는 의미 이외에는 별다른 개념은 없습니다.

직렬화 될 필드가 static, transient로 선언되어 있으면 직렬화할 때 제외됩니다.

static변수는 공유메모리 개념을 가지고 있기 때문에 직렬화 할 때 제외 됩니다.

 

 

---------------------------------------------------------------------------------------------------------

TransientTest.java

 

 

import java.io.*;

 

public class TransientTest implements Serializable
{


     // 멤버변수
     private String name;
     transient String passwd;

 

 // 생성자
 public TransientTest(String s, String p)
 {
      name = s;
      passwd = p;
      System.out.println("생성자가 호출되었습니다: " + name);
 }

 

 // toString() 메서드를 오버라이드하여
 // println() 메서드에서 사용할때, 내용을 출력하도록 변경
 public String toString()
 {
      return "이름은 " + name+ " : 패스워드 : " + passwd;
 }

 

 public static void main(String args[])
 {
      TransientTest tt1, tt2;
      tt1 = new TransientTest("김가방","1234");
      tt2 = new TransientTest("이치민","0011");

  try
  {
       // 객체직렬화로 파일에 저장하기 위해
       // FileOutputStream에서 ObjectOutputStream 생성
       ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("TransientTest.ser"));

      

       // writeObject() 메서드를 사용하여 객체 저장
       out.writeObject(tt1);
       out.writeObject(tt2);
       out.close();

 

       // 객체직렬화로 파일에 저장된 객체를 복원하기 위해
       // FileInputStream에서 ObjectInputStream 생성
       ObjectInputStream in = new ObjectInputStream(new FileInputStream("TransientTest.ser"));

   

       TransientTest tt3, tt4;

 

       // 해당 스트림에서 readObject() 메서드를 호출
       tt3 = (TransientTest)in.readObject();
       tt4 = (TransientTest)in.readObject();

       System.out.println("다시 복원합니다");

 

        // 내용을 출력한다
       System.out.println(tt3);
       System.out.println(tt4);
  }catch(Exception e) {e.printStackTrace();}
 }
}

 

 

======================================================

출력내용

 

생성자가 호출되었습니다: 김가방
생성자가 호출되었습니다: 이치민
다시 복원합니다
이름은 김가방 : 패스워드 : null
이름은 이치민 : 패스워드 : null

-----------------------------

name 멤버변수는 그대로 복원이 되었으나,

transient로 되어있는 passwd 멤버변수는 null값이 들어있다.

 

 

 

* Externalizable

 

객체직렬화의 또 다른 방법으로는 Externalizable인터페이스를 사용하는 것입니다.

그 기본 개념은 Serializable과 같습니다.

Externalizable자체가 Serializable인터페이스를 상속한 인터페이스이기 때문입니다. 인터페이스는 인터페이스 끼리는 상속의 개념이 적용됩니다.

 

그래서 그 Externalizable의 원형은 다음과 같습니다.

 

public interface Externalizable extends Serializable {

   public void writeExternal(ObjectOutput out) throws IOException;

 

   public void readExternal(ObjectInput in) throws IOException,

                                                                             ClassNotFoundException;

}

 

Externalizable 인터페이스는 2개의 메서드를 구현해야만 사용 가능합니다.

그리고 Serializable보다 미세한 직렬화를 다루기 위해서 사용됩니다.

Serializable에서는 자동으로 데이터가 기록되지만

Externalizable에서는 기록하는 부분을 직접 제어합니다.

 

이 때 기록하는 부분은 writeExternal() 메서드에 구현을 하며

읽어내는 부분은 readExternal() 메서드에 만들어 줍니다.

writeExternal() 메서드에서 사용자가 임의로 기록하는 방법을 구현했다면

읽어내는 해답을 갖고 있는 것은 writeExternal()을 구현한 개발자 자신입니다.

거의 암호화의 개념에 가깝다.

 

 

-------------------------------------------------------------------------

ExternalObject.java

 

 

import java.io.*;

 

public class ExternalObject implements Externalizable {

 

   private int dept;               // 부서

   private String name;       // 이름

   private float duties;         // 직책

 

  public ExternalObject(){}

 

   public ExternalObject(int dept, String name, float duties) {

       this.dept = dept;

       this.name = name;

       this.duties = duties;

   }

   public void readExternal(ObjectInput in) throws IOException,

                                                                             ClassNotFoundException

{

       System.out.println("readExternal() 메서드입니다.");

       dept = in.readInt();

       name = (String)in.readObject();

       duties = in.readFloat();

   }

   public void writeExternal(ObjectOutput out) throws IOException {

       System.out.println("writeExternal() 메서드입니다.");

       out.writeInt(dept);

       out.writeObject(name);

       out.writeFloat(duties);

   }

   public String toString(){

       return dept + ":" + name + ":" + duties;

   }

}

 

 

매개변수로 넘어오는 ObjectOutput의 객체 out을 이용하여 기록하고 싶은 부분을 차례대로 write해주고 있습니다.

물론 필요로 하는 대부분의 write메서드는 이미 존재합니다.

여기서는 간단히 int, String, float만을 기록하였지만

ObjectOutput 인터페이스가 제공해주는 writeBoolean, writeByte, writeBytes, writeChar, writeChars, writeDouble, writeFloat, writeInt, writeLong, writeShort, writeUTF 메서드들을 전부 사용할 수 있습니다.

그리고, 다시 이것을 읽어 오는 부분은 기록한 차례대로 읽어오면 됩니다.

 

정확하게 순서를 맞추어 호출해 주어야 합니다.

만약 이것의 순서를 바꾼다면  에러를 만나게 될 것입니다.

그리고 마지막으로 직렬화된 데이터를 읽어들여서 객체를 만들기 위해 인자없는 생성자가 필요합니다.

이것을 만들어주지 않으면 에러메시지에서 인자없는 생성자를 요구할 것입니다

 

 

---------------------------------------------------------------------------

ExternalObjectTest.java

 

 

import java.io.*;

 

public class ExternalObjectTest {

      

   public static void main(String[] args) throws IOException, ClassNotFoundException{

 

       FileOutputStream fileout= new FileOutputStream("exTest.txt");

       ObjectOutputStream out= new ObjectOutputStream(fileout);

       ExternalObject eo1 = new ExternalObject(1, "김사양", 170.25f);

       ExternalObject eo2 = new ExternalObject(2, "이거지", 190.01f);

       ExternalObject eo3 = new ExternalObject(3, "삼다수", 180.34f);

       out.writeObject(eo1 );

       out.writeObject(eo2 );

       out.writeObject(eo3 );

       out.close();

 

       FileInputStream filein = new FileInputStream("exTest.txt");

       ObjectInputStream in = new ObjectInputStream(filein);

       ExternalObject eso1 = (ExternalObject)in.readObject();

       ExternalObject eso2 = (ExternalObject)in.readObject();

       ExternalObject eso3 = (ExternalObject)in.readObject();

       System.out.println(eso1.toString());

       System.out.println(eso2.toString());

       System.out.println(eso3.toString());

       ois.close();

   }

}

+ Recent posts