JAVA2009. 11. 6. 00:04

//Java Serializatoin 이란?
자바 객체를 저장 또는 전송하기 위하여 자바 객체의 코드를 다시 복원가능한 형태의 Stream으로 직렬화 시켜주는 것을 말함!! 

 

기존예제 테스트 및 압축만!!

 

 

Java Serialization 알자

이 자료는 자바랜드(www.javaland.co.kr)의 박정기께서 기고하신 내용입니다. 

 

Java Serialization맛보기

Java Serializatoin은 자바 객체를 저장하거나 전송하기 위하여 자바 객체의 코드를 다시 복원가능한 형태의 Stream으로 직렬화 시켜주는 것을 말한다. 

 

가장 간단한 형태부터 시작해서 자바 시리얼라이제이션의 예를 살펴 보도록 하겠다. 

-Swrite.java-

//Java Serializatoin 이란?
//자바 객체를 저장 또는 전송하기 위하여 자바 객체의 코드를
//다시 복원가능한 형태의 Stream으로 직렬화 시켜주는 것을 말함!! 
package serializable;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.util.Date;

public class swrite {

 public static void main(String args[])

 {

  try {
   //파일이나 데이터를 출력하기위한 출력스트림을 생성한다.
   //tmp라는 파일을 갖는 f라는 객체 생성
   FileOutputStream f = new FileOutputStream("tmp");
   //기본데이터 타입의 데이터를 쓰는 객체 s 생성
   ObjectOutput s = new ObjectOutputStream(f);
   //Object타입의 객체를 쓴다. 인자는 문자열 타입의 "Today"
   s.writeObject("Today");
   //데이터 타입을 객체로 쓴다.
   s.writeObject(new Date());
   //스트림을 보내다. 메모리해제!! 및 tmp 파일로 생성됨
   s.flush();

  }

  catch (IOException e) {
  }
  
  //투데이라는 문자열 출력
  System.out.println("Today");
  //현재의 날짜를 출력
  System.out.println(new Date());

 }
}//class

 

위의 프로그램은 File Stream을 열어서 tmp라는 파일에 2개의 객체("Today"라는 String 객체와 Date 객체)를 저장하고 있다.  

여기서 객체저장을 위해 writeObject라는 메쏘드가 사용되고 있다는 것을 알 수 있다. 

 

위의 프로그램을 실행하면 2 객체가 시리얼라이제이션이 일어나서 다시 복원가능한 형태로되어 직렬화되어

tmp라는 파일로 저장된다. 

 

객체를 파일로 저장하다니? 얼마나 놀라운가? 

(제대로 다시 복원만 된다면...) 

 

자! 그럼 tmp 파일에서 객체를 살려보자!!! 

 

-Sread.java-

package serializable;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.util.Date;

public class sread {

 public static void main(String args[]) {
  try {
   // 파일 입력을 받기위한 파일 입력 스트림 in 객체 생성
   // 인자는 파일 또는 "tmp"문자열 사용 가능
   FileInputStream in = new FileInputStream("tmp");

   // 입력받은 in객체를 입력을 받기위한 객체 s로 만듬
   ObjectInput s = new ObjectInputStream(in);

   // 읽어들인 객체를 문자열 today에 저장
   String today = (String) s.readObject();
   // 읽어들인 객체를 Date 타입의 date객체로 저장
   Date date = (Date) s.readObject();

   System.out.println(today);

   System.out.println(date);

  } catch (IOException e) {
  } catch (ClassNotFoundException e) {
  }
 }
}// class

위의 파일은 아까 저장한 tmp파일에서 객체를 복원하여  

today와 date가 저장 직전의 상태로 복원되었다. 

여기서 readObject라는 메쏘드가 사용되었음을 알 수 있다. 

 

객체를 저장한 tmp 파일은 직렬화되어있으므로  

그냥 내용을 사용자가 알 수 없다. 

하지만 2 객체를 훌륭하게 저장하였다는 것을 알 수 있다.

 

Java Serializatoin의 개념 이해

앞의 예제는 Java Serializatoin의 맛빼기였다. 

그냥 대충 돌아가는 것만 보여준 것이지 개념상  

부족한 내용이 많다. 

 

좀더 심화된 내용으로 Java Serialization을 제대로 이해해 보자! 

 

Java Object Serialization은 자바 객체를 저장 또는 전송을 위하여 자바 코드를 다시 복원 가능한 byte stream 형태로 변환시켜 준다. 이 직렬화 과정을 자세히 말하면 

객체가 다시 원상태로 복원되기 위해서는 객체 내부의 

data들의 reference가 잘 정리되어야 있어야 한다.  

이러한 과정은 직렬화를 통하여 object reference의 tree 

즉, object graph를 형성하므로써 가능하다. 

이 graph 정보를 이용해서 객체를 다시 복원할 수 있는 것이다. 

 

이제 객체를 byte stream으로 변환되는 과정을 marshaling이라 부르고, 반대로 stream에서 객체로 역변환하는 과정을 unmarshaling이라고 한다. 

 

또한 객체가 안전하게 직렬화되기 위해서는 

해당 클래스가 Serializable 인터페이스를 imeplements하고 있어야만 한다. 

 

앞의 강좌 1의 예제에서는... 

알다시피 String 클래스와 Date 클래스 모두 API 레퍼런스를 보면 이미 Serializable 인터페이스를 imeplements하고 있을 확일할 수 있다. 

 

자! 그럼... 

 

이번엔 사용자가 정의한 클래스를 직렬화시켜서 

파일로 저장하고 다시 복원하는 예제를 보도록 하자! 

 

먼저 전송할 사용자가 만든 Test 클래스를 다음과 같이 만든다. 

 

-Test.java-

 

package serializable;

public class Test {

 //전역 문자열 변수 str
 public String str;
 
 //transient
 //나중에 다시 사용하기 위한 저장을 하지 않는,
 //짧고 제한된 수명을 갖는 소프트웨어 객체이다.

 //전력선을 통해 오직 몇 분의 1초 동안 지속되는
 //갑작스러운 여분의 전압 펄스가 들어오는 것으로서,
 //컴퓨터나 파일 등에 손상을 입힐 수 있다.
 public transient int ivalue;

 public Test(String s, int i) {
  this.str = s;
  this.ivalue = i;
 }
}//class

당연히 java.io.Serializable 인터페이스를 implements하고 있어야 한다. 

위에서 Test 클래스에 멤버변수로 String 타입의 str과 int 타입의 ivalue가 있음을 볼 수 있다. 

그런데 이기서 transient 키워드에 주목하자! 

transient 키워드는 앞에 지정하면 지정된 항목 내용은  

자바 시리얼라이제이션에서 제외된다.  

즉, 직렬화가 이루어질때 사용자가 문제의 소지가 있는 변수나 메쏘드를 제외시킬 수 있도록 해주는 것이다. 

 

그럼 Test 클래스를 직렬화하여 파일로 저장해보자! 

 

-Write.java-
package serializable;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class write {
 public static void main(String args[]) {

  try {
   //file.out이란 이름으로 파일 fos 객체 스트림을 생성
   FileOutputStream fos = new FileOutputStream("file.out");
   //출력스트림 객체 생성
   ObjectOutputStream oos = new ObjectOutputStream(fos);
   //태스트클래스를 이용하여 문자열, 숫자 타입의 객체를 인자로 넘겨 객체 생성
   oos.writeObject(new Test("testing", 37));
   //메모리 해제 및 파일 생성
   oos.flush();
   //스트림을 사용하였으므로ㅌ 스트림을 닫는다.
   fos.close();
  }
  catch (Throwable e){
   //출력 결과들 출력
   System.err.println(e);
  }
 }
}//class

위의 소스는 Test 객체를 file.out이라는 이름의 파일로 저장할 것이다. 

역시 writeObject를 사용했고, 초기 값으로 "testing" 이라는 문자열과 37의 값을 지정했다. 

 

자! 그럼 file.out 파일에서 Test 객체로 복원시켜 보자! 

 

-Read.java-

package serializable;

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class read {

 public static void main(String args[]) {
  //테스트 클래스 타입의 객체 생성
  Test testobj = null;
  try {
   //file.out이란 파일을 읽어 입력스트림으로 변환
   FileInputStream fis = new FileInputStream("file.out");
   //파입입력스트림을 객체입력스트림 객체로 생성
   ObjectInputStream ois = new ObjectInputStream(fis);
   //읽어들인 객체를 테스트타입크랠스 객체로 저장
   testobj = (Test) ois.readObject();
   //파일 입력이 완료되었으므로 닫는다.
   fis.close();
  }
  catch (Throwable e){
   e.printStackTrace();
   //System.err.println(e);
  }
  //변환된 스트림을 출력하기 위해 만들어진 객체 타입의 필드값을 출력
  System.out.println(testobj.str);
  System.out.println(testobj.ivalue);
 }//method
}//class

위의 소스를 컴파일하여 실행시켜 보면 

file.out 파일에서 readObject 메쏘드를 사용해서 Test 객체를 복원해 낸다. 

 

결과를 출력해보면 다음과 같다!!! 

 

testing 

 

여기서 testobj.str의 값은 저장하기 이전 상태 그대로지만 testobj.ivalue의 값은 처음에 37로 지정했는데 0으로 출력되었다. 

 

이것은 test 클래스에서 transient 키워드가 지정되었기 때문에 시리얼라이제이션에서 제외되었기 때문에 본래 값을 보존할 수 없었던 것이다.

 

Java Object Serializatoin

Java Object Serializatoin의 3회 강좌를 시작한다. 

이번에는 객체를 소켓을 통해서 전송하는 방법에 대해서 다루겠다. 

이때 주의해할 점이 있는데... 이것은 

예제를 통해서 공부해 보도록 하겠다. 

 

먼저 전송할 객체 클래스를 다음과 같이 만들었다고 하자! 

 

-MyObject.java-

 

package serializable;

import java.io.Serializable;

public class MyObject implements Serializable {
 
 //객체 생성
 String name;
 int count;

 //기본 생성자
 MyObject() {
  setName();
 }

 //setter
 public void setName() {
  count++;
  name = "MyObject " + count;
 }

 //이름을 반환하는 toString메소드 재정의
 public String toString() {
  return name;
 }
}

역시... Serializable 인터페이스를 implements하고 있으며... 변수로 name과 count를 가지고 있다. 

컨스트럭터에서 setName() 메쏘드를 호출하여 count를 증가시키고 name의 String에도 숫자가 추가되고 있다. 

 

그러면 위의 객체를 스트림을 통하여 소켓으로 전송하기위하여 먼저 서버를 만들어보자! 

 

-JabberServer.java-

package serializable;

import java.io.DataInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class JabberServer {

 //포트 설정
 static final int port = 8080;

 public static void main(String[] args) {
        try {
         //빈 객체 생성
             MyObject o = new MyObject();
             //서버소켓 생성
             ServerSocket s = new ServerSocket(port);
             //생성된 서버 출력
             System.out.println("Server Started: " + s);
             //서버소켓에 접속이 이루어졌을 때 소켓을 생성
             Socket socket = s.accept();
             //접속한 클라이언트의 상태를 출력
             System.out.println("Connection accepted, socket: " + socket);
             // 접속한 클라이언트의 출력스트림을 갖는 출력 스트림 객체를 생성
             ObjectOutputStream ToClient =
                                  new ObjectOutputStream(
                                        socket.getOutputStream());
             //소켓의 입력스트림을 받는 데이터 입력스트림 객체를 생성
             DataInputStream FromClient =
                                  new DataInputStream(
                                        socket.getInputStream());
             //오브젝트객체의 카운트가 10이될 때까지 반복
             while (o.count<11) { 
              //오브젝트 객체를 사용한 횟수 출력
                  System.out.println("writing " + o);
                  //이름 설정
                  o.setName(); 
                  /**
                    Object reference를 reset한다.
                    (이 부분이 포인트)
                    이것을 안하면 첫번째 전송한 객체의 
                    reference로  계속 전송된다.
                    그래서 갱신된 data가 반영되지 못하는
                    현상이 생긴다.
                  */
                  //서버를 리셋
                  ToClient.reset();
                  //빈객체를 클라이언트객체에 쓴다.
                  ToClient.writeObject(o);
                  System.out.print("trying to received acknowledgement ... ");
                  //클라이언트에서 읽은 인트 값을 출력한다.
                  System.out.println("acknowledgement: " +
                                             FromClient.readInt()); 
                  System.out.println("succeeded");
             }
             System.out.println("closing...");
             //클라이언트의 연결을닫는다.
             ToClient.close();
             //서버 소켓을 닫는다.
             socket.close();
        } 
        catch(Exception e){
             e.printStackTrace();
        } 
    }
}

위 서버 프로그램에서는 서버 소켓을 8080 포트로 열어놓고 클라언트를 기다리다가 클라이언트가 억셉트되면 계속 동작하게 되어있다. 

 

계속 설명하기 전에... 클라이언트 프로그램도 같이 보도록 하자! 

 

-JabberClient.java-

 

package serializable;

import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.ObjectInputStream;
import java.net.InetAddress;
import java.net.Socket;

public class JabberClient {

 static final int port = 8080;

 public static void main(String args[]) {
        MyObject o;

        try 
        {
         //아이피 객체 생성, 이름은 없다~
            InetAddress addr = InetAddress.getByName(null);
            //얻어진 아이피 주소를 출력함.
            System.out.println("addr = " + addr);
            //네트워크를 사용할 소켓 생성, 아이피 주소와 포트번호로 한다
            Socket socket = new Socket(addr, port);
            System.out.println("socket = " + socket);
            //객체를 입력할 입력스트림 생성,
            ObjectInputStream FromServer = new
            ObjectInputStream(socket.getInputStream());
            //서버로 전송할 데이터출력 스트림을 생성
            DataOutputStream ToServer = new
            DataOutputStream(socket.getOutputStream());
 
            //카운트할 변수 선언
            int i=0; 

            while(true){
             //빈 객체에 서버로부터 읽어온 객체를 입력
                o = (MyObject)FromServer.readObject();
                System.out.print("trying to send acknowledgement ... ");
                //스레드 생성, 대기시간은 0.5초
                Thread.sleep(500);
                //출력스트림에 증가된 i값을 증가시켜서 보내줌
                ToServer.writeInt(i++);
                System.out.println("succeeded");
                System.out.println(o);
            }
       } 
       catch (EOFException f){
            System.exit(0);
       } 
       catch(Exception e){
            e.printStackTrace();
       }
   }
}//class

소켓이 맺어진 후에... 

 

서버에서 MyObject 객체를 생성하면 컨스트럭터에의해 카운트가 1이 된다. 

그후 setName() 메쏘드를 호출하고 나면 카운트가 2가 되고  이것을 스트림으로 바꾸어 객체를 전송하게 된다. 

 

클라이언트는 전송된 객체를 받아 ACK 성공 메시지를 뿌리고 카운트 값을 찍는다. 

 

그후 서버에서 setName() 메쏘드를 다시 호출하여 객체의 카운트 상태값을  하나 증가시켜 3으로 만든후 객체를 스트림으로 전송한다. 

 

클라이언트는 두번째 객체를 받아 ACK 성공 메시지를 뿌리고 카운트 값을 찍는다. 

이때 카운트 값이 갱신된 3값으로 찍혀야 하는데... 

여전히 2로 찍혀져 나온다? 

 

왜일까???    

 

이것은 자바의 버그가 아니다. 

 

이것은 자바 시리얼라이제이션에서는 

처음 전송한 객체의 object reference를 인위적으로

reset시켜 주지 않으면 객체의 상태가 바뀌더라도 전송되는 객체의 object reference는  처음 전송한 객체의 object reference로 계속 가지게 되므로  이와 같은 현상이 발생한다. 

 

이문제를 해결하려면... 

객체를 스트림으로 전송하기 전에... 

reset() 메쏘드를 통하여 같은 object reference를 사용하지 않도록 리셋시켜주어야 한다. 

 

즉, 

ToClient.reset(); 부분에 대한 이해가 이번 강좌의 포인트이다. 

이부분이 있고, 없고의 차이를 반드시 이해하기 바란다. 

 

객체를 소켓으로 계속 전송하게 될 경우... 

실수하면 위와같은 버그아닌 오류에 봉착하기 쉽다. 


'JAVA' 카테고리의 다른 글

overriding 실습 (ex:할아버지/아버지/아들)  (0) 2009.11.21
자바 중간고사 정리  (0) 2009.11.21
자바의 기본 타입  (0) 2009.11.06
자바 기초 문법  (0) 2009.11.06
2009/01/05 RealChoky 어록  (0) 2009.11.05
Posted by Tiwaz