'JAVA PROGRAMMING/JAVA'에 해당되는 글 59건

  1. 2010/02/21 12.3 Externalizable
  2. 2010/02/21 12.2 Serializable
  3. 2010/02/21 12.1 Serialization
  4. 2010/02/21 11.3 리플렉션 프로그래밍
  5. 2010/02/21 11.2 정적 바인딩 클래스와 동적 바인딩 클래스

12.3 Externalizable

JAVA PROGRAMMING/JAVA 2010/02/21 22:10 by 킨테리 KinTeL



12.3.1 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 인터페이스는 Serializable 인터페이스보다 약간 복잡합니다. Serializable은 메서드를 포함하지 않은 표시 인터페이스이기 때문에 implements Serializable만 붙여주면 되지만, Externalizable 인터페이스는 2개의 메서드를 포함하고 있기 때문에 2개의 메서드를 구현해야만 직렬화가 가능합니다. 보통의 경우 Serializable 인터페이스보다 미세한 직렬화를 다루기 위해서 사용합니다.

▣ Externalizable의 구현
◈ 사용자가 직접 객체의 직렬화를 위한 구현을 해야 한다.

Serializable에서는 자동으로 데이터가 기록되지만 Externalizable에서는 2개의 메서드를 구현해야 하며, 이 두 개의 메서드에 데이터를 읽어오고 기록하는 구현을 사용자가 직접 하게 됩니다. 즉 객체를 저장하고 읽어오는 부분을 직접 제어하겠다는 것입니다. 이 때 기록하는 부분은 writeExternal()를 사용하고, 읽어내는 부분은 readExternal()을 사용합니다. 

▣ Externalizable 인터페이스
◈ writeExternal() 메서드 : 기록하는 부분을 제어
◈ readExternal() 메서드 : 읽어내는 부분을 제어

말로만 해서 무슨 소용이 있겠습니까! 직접 예제를 만들어서 확인해 보도록 하겠습니다. 다음은 Serializable 대신에 Externalizable을 구현한 예입니다.

『chap12\ExternalObject.java』
ⓙ───────────────────────────────────────
/**
Externalizable을 구현한 예
**/
import java.io.*;
public class ExternalObject implements Externalizable {
    private int id; //부서
    private String name; //이름
    private float height; //신장
    public ExternalObject(){}
    public ExternalObject(int id, String name, float height) {
        this.id = id;
        this.name = name;
        this.height = height;
    }
    public void readExternal(ObjectInput oi) 
            throws IOException, ClassNotFoundException {
        System.out.println("readExternal() 메서드 호출");
        id = oi.readInt();
        name = (String)oi.readObject();
        height = oi.readFloat();
    }
    public void writeExternal(ObjectOutput oo) throws IOException {
        System.out.println("writeExternal() 메서드 호출");
        oo.writeInt(id);
        oo.writeObject(name);
        oo.writeFloat(height);
    }
    public String toString(){
        return id + ":" + name + ":" + height;
    }
} //end of ExternalObject class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac ExternalObject.java
***/
───────────────────────────────────────ⓑ

Externalizable 인터페이스를 구현하는 순서는 다음과 같습니다. 

▣ Externalizable 인터페이스를 구현하는 순서
◈ 1. implements Externalizable
◈ 2. public void writeExternal(ObjectOutput oo) 메서드 구현
◈ 3. public void readExternal(ObjectInput oi) 메서드 구현
◈ 4. 매개변수 없는 생성자 구현(반드시)

이러한 절차에서 구현한 위의 예제를 보시면 implements Externalizable부터 붙인 후에 readExternal()과 writeExternal()를 구현하고 있습니다. 이들 메서드의 내부를 한번 보도록 하겠습니다. 먼저 writeExternal()부터 보도록 하겠습니다.

▣ writeExternal()의 구현
◈ public void writeExternal(ObjectOutput oo) throws IOException {
◈         System.out.println("writeExternal() 메서드 호출");
◈         oo.writeInt(id);
◈         oo.writeObject(name);
◈         oo.writeFloat(height);
◈ }

이 부분은 매개변수로 넘어오는 ObjectOutput의 객체 oo를 이용해서 기록하고 싶은 데이터를 차례대로 기록해 주고 있습니다. 물론 여러분들이 필요로 하는 대부분의 writeXXX() 메서드는 이미 존재합니다. 여기서는 간단히 int, String, float만을 기록하였지만 ObjectOutput 인터페이스가 제공해 주는 writeBoolean(), writeByte(), writeBytes(), writeChar(), writeChars(), writeDouble(), writeFloat(), writeInt(), writeLong(), writeShort(), writeUTF() 등을 전부 사용할 수 있습니다.

그리고 이것을 다시 읽어오는 방법은 기록한 차례대로 읽어들이면 됩니다. readExternal() 내부는 다음과 같습니다. 

▣ readExternal()의 구현
◈ public void readExternal(ObjectInput oi) throws IOException, ClassNotFoundException {
◈         id = oi.readInt();
◈         name = (String)oi.readObject();
◈         height = oi.readFloat();
◈ }

정확하게 순서를 맞추어 호출해 주어야 합니다. 만약 여러분들이 이것의 순서를 바꾼다면 에러를 만나게 될 것입니다. 

▣ writeExternal()과 readExternal()의 데이터를 기록하고 읽는 순서
◈ writeExternal()로 데이터가 기록된 정확한 순서대로 readExternal()를 이용해서 읽어내야 한다.

그리고 마지막으로 직렬화된 데이터를 읽어들여서 객체를 만들기 위해 인자 없는 생성자가 필요합니다. 이것을 만들어 주지 않으면 에러 메시지에서 인자 없는 생성자를 요구할 것입니다.

▣ 매개변수 없는 생성자
◈ public ExternalObject(){}

▣ 디폴트 생성자를 주석 처리한 후 실행한 결과
◈ writeExternal() 메서드 호출
◈ writeExternal() 메서드 호출
◈ writeExternal() 메서드 호출
◈ Exception in thread "main" java.io.InvalidClassException: ExternalObject; no valid constructor
◈         at ObjectStreamClass.<init>(ObjectStreamClass.java:375)
◈         at ObjectStreamClass.lookup(ObjectStreamClass.java:249)
◈         at ObjectOutputStream.writeObject0(ObjectOutputStream.java:1010)
◈         at ObjectOutputStream.writeObject(ObjectOutputStream.java:278)
◈         at ExternalObjectMain.main(ExternalObjectMain.java:9)

위의 순서대로 예제를 구현하면 다음과 같습니다.

『chap12\ExternalObjectMain.java』
ⓙ───────────────────────────────────────
/**
Externalizable을 이용한 직렬화를 테스트하는 예
**/
import java.io.*; 
public class ExternalObjectMain{        
    public static void main(String[] args) 
            throws IOException, ClassNotFoundException{
        FileOutputStream fos = new FileOutputStream("external.dat"); 
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        ExternalObject so1 = new ExternalObject(1, "홍길동", 170.25f);
        ExternalObject so2 = new ExternalObject(2, "김삿갓", 190.01f);
        ExternalObject so3 = new ExternalObject(3, "암행어", 180.34f);
        oos.writeObject(so1);
        oos.writeObject(so2);
        oos.writeObject(so3);
        oos.close();

        FileInputStream fis = new FileInputStream("external.dat");
        ObjectInputStream ois = new ObjectInputStream(fis);
        ExternalObject rso1 = (ExternalObject)ois.readObject();
        ExternalObject rso2 = (ExternalObject)ois.readObject();
        ExternalObject rso3 = (ExternalObject)ois.readObject();
        System.out.println(rso1.toString());
        System.out.println(rso2.toString());
        System.out.println(rso3.toString());
        ois.close();
    } //end of main
} //end of ExternalObjectMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac ExternalObjectMain.java
C:\javasrc\chap12>java ExternalObjectMain
writeExternal() 메서드 호출
writeExternal() 메서드 호출
writeExternal() 메서드 호출
readExternal() 메서드 호출
readExternal() 메서드 호출
readExternal() 메서드 호출
1:홍길동:170.25
2:김삿갓:190.01
3:암행어:180.34
***/
───────────────────────────────────────ⓑ

Externalizable 인터페이스를 테스트하는 예제는 일반 Serializable 인터페이스를 테스트하는 예제와 다른 것이 없습니다. 다만 직렬화 객체를 Externalizable 인터페이스로 구현했을 뿐입니다. 이로써 여러분은 직렬화의 두 가지 사용법인 Serializable과 Externalizable 인터페이스를 이용하는 방법을 모두 알아보았습니다.
저작자 표시 비영리 변경 금지

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

12.3 Externalizable  (0) 2010/02/21
12.2 Serializable  (0) 2010/02/21
12.1 Serialization  (0) 2010/02/21
11.3 리플렉션 프로그래밍  (0) 2010/02/21
11.2 정적 바인딩 클래스와 동적 바인딩 클래스  (0) 2010/02/21
11.1 Reflection  (0) 2010/02/21

12.2 Serializable

JAVA PROGRAMMING/JAVA 2010/02/21 22:10 by 킨테리 KinTeL



12.2.1 Serializable



앞에서는 객체 직렬화의 기본적인 개념과 간단한 원리를 알아보았습니다. 이번 절에서는 ObjectInputStream과 ObjectOutputStream을 사용해서 실제로 객체를 파일에 저장하고, 다시 객체를 복원하는 방법에 대해 알아보도록 하겠습니다. 그리고 transient를 사용해서 특정 멤버 필드가 직렬화되는 것을 방지하는 방법에 대해 살펴보겠습니다.

▣ 직렬화의 학습내용
◈ ObjectInputStream과 ObjectOutputStream을 이용한 직렬화
◈ transient를 이용한 직렬화 방지
◈ 직렬화 불가능한 경우

먼저 Serializable을 이용한 객체 직렬화의 구현 방법에 대해서 알아보도록 하겠습니다. Serializable의 구현은 일반 클래스에 implements Serializable만 붙여주면 됩니다. 이 인터페이스가 구현되면 객체의 저장이나 복원은 Object 스트림이 알아서 처리합니다.

제일 먼저 여러분은 Serializable을 구현한 클래스가 필요합니다. 여러분은 implements Serializable을 구현한 후 해당 클래스의 객체를 생성할 것입니다. 그리고 생성된 객체를 직렬화하려 할 것입니다.

▣ Serializable의 구현과 직렬화될 객체 생성
◈ public class SerialObject implements Serializable{
◈         //...
◈ }
◈ SerialObject so = new SerialObject(); //직렬화할 객체

Serializable이 구현된 클래스의 객체를 직렬화하기 위해서는 직렬화할 목표지점이 필요합니다. 직렬화를 위한 목표지점은 파일이 될 수도 있으며, 네트웍의 특정지점이 될 수도 있습니다. 현재는 파일 스트림에 객체를 저장하도록 하겠습니다. 그렇다면 다음과 같이 먼저 File 스트림을 생성해야 합니다. 

▣ 직렬화를 위한 FileOutputStream 생성
◈ FileOutuputStream fos = new FileOutputStream("serialobject.dat");

이렇게 생성된 스트림은 다시 Object 스트림으로 변환해야 합니다. 9장의 Java Stream에서 Object 스트림은 2차 스트림이라고 배웠으며, ObjectInputStream과 ObjectOutputStream 자체가 바이트 단위의 스트림이기 때문에 다음과 같이 FileOutputStream은 ObjectOutputStream으로 쉽게 변환될 수 있습니다.

▣ 직렬화를 위한 ObjectOutputStream 생성
◈ FileOutuputStream fos = new FileOutputStream("serialobject.dat");
◈ ObjectOutputStream oos = new ObjectOutputStream(fos);

최종적으로 ObjectOutputStream이 만들어졌다면 serialobject.dat 파일로 객체를 저장하면 됩니다. 이 때 ObjectOutputStream에서는 writeObejct(Object obj)를 이용해서 객체를 저장하게 됩니다.

▣ 객체 직렬화(Serialization)
◈ SerialObject so = new SerialObject(); //직렬화 객체의 생성
◈ FileOutuputStream fos = new FileOutputStream("serialobject.dat"); //파일 스트림 생성
◈ ObjectOutputStream oos = new ObjectOutputStream(fos); //객체 스트림으로 변환
◈ oos.writeObject(so); //객체 저장
◈ oos.close(); //스트림 닫기

물론 이 반대의 경우도 존재할 수 있습니다. 위의 serialobject.dat라는 파일에 저장된 객체를 읽어들인다고 가정하죠. 그러면 serialobject.dat에 FileInputStream을 생성한 후 ObjectInputStream으로 변환하고, readObject() 메서드를 이용해서 저장된 객체를 읽어들이면 됩니다. 

▣ 객체 역직렬화(Deserialization) 
◈ FileInputStream fis = new FileInputStream("serialobject.dat"); //파일 스트림 생성
◈ ObjectInputStream ois = new ObjectInputStream(fis); //객체 스트림으로 변환 
◈ Object obj = ois.readObject(); //객체 읽기
◈ SerialObject temp = (SerialObject)obj; //형복원
◈ ois.close(); //스트림 닫기

객체를 복원해서 읽을 들일 때 사용하는 readObject()의 리턴형이 Object형이기 때문에 다운캐스팅이 필요합니다. 이러한 일련의 과정을 순서대로 정리하면 다음과 같습니다.

▣ 객체 직렬화 과정
◈ 목표지점에 출력 스트림을 생성한다.
◈ 생성된 스트림을 Object 출력 스트림으로 변환한다.(ObjectOutputStream)
◈ 직렬화된 객체를 객체스트림을 통해서 전송하거나 읽어낸다.
◈ 예) ObjectOutputStream -> writeObject(Object obj)
◈ 스트림을 닫는다.

▣ 객체 역직렬화 과정
◈ 목표지점에 입력 스트림을 생성한다.
◈ 생성된 스트림을 Object 입력 스트림으로 변환한다.(ObjectInputStream)
◈ 객체 스트림을 통해서 직렬화된 객체를 읽어낸다.
◈ 예) ObjectInputStream -> readObject()
◈ 스트림을 닫는다.

▣ writeObject()와 readObject()    
◈ writeObject()는 객체를 스트림에 기록하는데 사용하고, readObject()는 스트림으로부터 객체를 복원할 때 사용한다.

▣ Serializable 인터페이스    
◈ 표시 인터페이스이기 때문에 구현해야 할 메서드가 없다. 이 인터페이스는 단지 붙여만 주면 된다.



12.2.2 객체 스트림



객체 스트림은 자바 IO에서 제공해 주기 때문에 여러분은 직렬화할 객체만 생각하면 됩니다. 자! 그렇다면 앞에서 배운 순서대로 객체 직렬화를 구현해 보도록 하겠습니다. 여러분이 할 일은 직렬화의 대상이 되는 클래스에 impelements Serializable만 붙여주면 됩니다.

『chap12\SerialObject.java』
ⓙ───────────────────────────────────────
/**
Serializable을 이용한 객체 직렬화의 구현
**/
import java.io.*;
public class SerialObject implements Serializable {
    private String name; // 이름
    private String dept; // 부서
    private String title; // 직책
    public SerialObject (String name, String dept, String title) {
        this.name = name;
        this.dept = dept;
        this.title = title;
    }
    public String toString() {
        return name + ":" + dept + ":" + title;
    }
} //end of SerialObject class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac SerialObject.java
***/
───────────────────────────────────────ⓑ

위의 예제는 너무나 단순한 예제입니다. 단지 implements Serializable을 붙였다는 것밖에는 일반 클래스와 다른 점이 없습니다. 

자! 이제 임의의 파일에 객체 스트림을 연결해서 객체를 읽고 기록해 보도록 하겠습니다. 예리한 분들은 '직렬화될 클래스의 멤버 변수들이 모두 private으로 선언되었는데, 이것은 상관없을까?' 하는 의구심이 생길 것입니다. 직렬화는 접근지정자에 상관 없이 수행된답니다. 그러니 걱정하지 마세요. 아래의 예제는 객체를 기록한 후, 다시 읽어내는 예를 보여주고 있습니다.

『chap12\SerialObjectMain.java』
ⓙ───────────────────────────────────────
/**
SerialObject를 이용해서 객체를 기록한 후 다시 읽어내는 예
**/
import java.io.*; 
public class SerialObjectMain {
    public static void main(String[] args) throws Exception {
        //직렬화를 이용한 객체 저장
        FileOutputStream fos = new FileOutputStream("serialobject.dat"); 
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        SerialObject so1 = new SerialObject("홍길동", "총무부", "과장");
        SerialObject so2 = new SerialObject("김삿갓", "영업부", "과장");
        SerialObject so3 = new SerialObject("암행어", "인사부", "과장");
        oos.writeObject(so1);
        oos.writeObject(so2);
        oos.writeObject(so3);
        oos.close();
        //직렬화를 이용한 객체 복원
        FileInputStream fis = new FileInputStream("serialobject.dat");
        ObjectInputStream ois = new ObjectInputStream(fis);
        SerialObject rso1 = (SerialObject)ois.readObject();
        SerialObject rso2 = (SerialObject)ois.readObject();
        SerialObject rso3 = (SerialObject)ois.readObject();
        System.out.println(rso1);
        System.out.println(rso2);
        System.out.println(rso3);
        ois.close();
    } //end of main
} //end of SerialObjectMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac SerialObjectMain.java
C:\javasrc\chap12>java SerialObjectMain
홍길동:총무부:과장
김삿갓:영업부:과장
암행어:인사부:과장
***/
───────────────────────────────────────ⓑ

먼저 serialobject.dat 파일에 File 출력 스트림을 생성합니다. 그리고 이 File 출력 스트림을 다음과 같이 Object 출력 스트림으로 변환합니다. 

▣ ObjectOutputStream 생성
◈ FileOutputStream fos = new FileOutputStream("serialobject.dat"); 
◈ ObjectOutputStream oos = new ObjectOutputStream(fos);

스트림이 개설되었다면 직렬화가 가능한(implements Serializable로 구현된) 객체를 만들어야겠죠. 위의 예제에서는 3개의 객체를 준비했습니다.

▣ 직렬화를 위한 객체 생성
◈ SerialObject so1 = new SerialObject("홍길동", "총무부", "과장");
◈ SerialObject so2 = new SerialObject("김삿갓", "영업부", "과장");
◈ SerialObject so3 = new SerialObject("암행어", "인사부", "과장");

그리고 이 객체들을 serialobject.dat 파일에 기록하는 부분은 다음과 같습니다. 

▣ 스트림을 이용한 객체의 직렬화
◈ oos.writeObject(so1);
◈ oos.writeObject(so2);
◈ oos.writeObject(so3);

객체가 3개이기 때문에 3번 기록해야겠죠. 마지막으로 출력 스트림을 닫습니다. 이 부분까지 수행되면 serialobject.dat가 만들어지고, 객체 3개가 순서대로 기록됩니다. 소스를 간단하게 하기 위해서 저장한 객체를 바로 역직렬화하고 있습니다. 우선, 객체를 읽어내기 위해서 serialobject.dat 파일에 File 입력 스트림을 생성합니다. 그리고 생성된 File 입력 스트림을 Object 입력 스트림으로 변환합니다. 

▣ 역직렬화를 위한 ObjectInputStream의 생성
◈ FileInputStream fis = new FileInputStream("serialobject.dat");
◈ ObjectInputStream ois = new ObjectInputStream(fis);

변환된 Object 입력 스트림으로 객체를 읽어냅니다. 앞에서 3번 저장했으니 3번만 읽어내면 됩니다. 그리고 Object 입력 스트림으로 읽을 때, 반환형이 Object형이기 때문에 여러분은 강제 다운캐스팅시켜서 사용해야 합니다.

▣ ObjectInputStream을 통해서 객체 읽어내기
◈ SerialObject rso1 = (SerialObject)ois.readObject();
◈ SerialObject rso2 = (SerialObject)ois.readObject();
◈ SerialObject rso3 = (SerialObject)ois.readObject();

실제 이 객체들을 이용할 수 있는지 SerialObject 클래스의 멤버 메서드를 호출하는 부분은 다음과 같습니다.

◈ System.out.println(rso1.toString());
◈ System.out.println(rso2.toString());
◈ System.out.println(rso3.toString());

마지막으로 Object 입력 스트림을 닫으시면 모든 작업은 끝이 납니다. 위의 예제에서 ObjectOutputStream과 ObjectInputStream에 대해서 조금 더 알아보도록 하죠.

Object 스트림은 스트림의 한 종류입니다. 객체를 직렬화시켜서 저장하는 스트림이라고 말할 수 있습니다. 객체를 객체 Object 출력 스트림에 기록할 때는 ObjectOutputStream 클래스의 writeObject(Object obj) 메서드를 사용합니다. writeObject 메서드의 원형은 다음과 같다.

▣ writeObject() 메서드의 원형
◈ public final void writeObject(Object obj) throws IOException

writeObject(Object obj) 메서드의 인자로 넘어 온 객체가 Serializable 인터페이스나 Externalizable 인터페이스를 구현했는지 검사합니다. 주어진 객체가 Serializable 인터페이스를 구현했다면, writeObject() 메서드는 자동으로 객체의 상태를 스트림에 기록하게 됩니다. 만약 객체가 Serializable이나 Externalizable 인터페이스 중 어느 것도 구현하지 않았다면, NotSerializableException을 발생시킵니다.

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

▣ readObject() 메서드의 원형
◈ public final Object readObject() throws OptionalDataException, ClassNotFound Exception, IOException

readObject()는 연결된 스트림으로부터 객체의 상태 정보를 읽어내고, writeObject() 메서드와 반대로 readObject() 메서드는 스트림에 기록되어 있는 객체의 상태 정보를 기반으로 원래의 객체로 복원해 줍니다.



12.2.3 transient



객체 스트림을 이용해서 직렬화할 때 객체의 모든 상태정보 즉 멤버 필드를 직렬화하게 됩니다. 하지만 클래스를 디자인하다 보면 순간적으로 사용하고 버려야 하는 필요 없는 정보도 있습니다. 이러한 정보를 제외시키기 위해서 transient 키워드를 사용합니다.

클래스를 만들다 보면 중요하지는 않지만, 클래스 내에서 전역 변수로 사용하기 위해서 어쩔 수 없이 멤버 변수로 만드는 경우가 있습니다. 여러분도 클래스를 웬만큼 만들어 보았다면 이런 경험은 흔할 것입니다. 이러한 정보까지 직렬화할 필요는 없습니다. 이 때 저장할 필요가 없다고 생각된다면 접근지정자 다음에 transient를 붙이면 직렬화에서 제외됩니다.

▣ transient 키워드
◈ 멤버 변수를 직렬화의 대상에서 제외할 때 사용하는 키워드
◈ 멤버 변수 앞에 transient를 명시

transient는 해당 멤버 변수를 직렬화에서 제외시키기 때문에 역직렬화했을 때 멤버 변수의 값은 무조건 디폴트 값으로 초기화됩니다. 객체의 경우 디폴트로 null을, int는 0으로 설정하게 됩니다. 아래의 예제는 transient의 예를 보여주고 있습니다.

『chap12\TransientObjectMain.java』
ⓙ───────────────────────────────────────
/**
transient를 이용해서 멤버 변수를 직렬화에서 제외시키기
**/
import java.io.*;
class TransientObject implements Serializable {
    private String name;        // 이름
    private String dept;        // 부서
    private transient String title;        // 직책
    public TransientObject (String name, String dept, String title) {
        this.name = name;
        this.dept = dept;
        this.title = title;
    }
    public String toString() {
        return name + ":" + dept + ":" + title;
    }
} //end of TransientObject class

public class TransientObjectMain{        
    public static void main(String[] args) throws Exception {
        FileOutputStream fos = new FileOutputStream("serialobject2.dat"); 
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        TransientObject so1 = new TransientObject("홍길동", "총무부", "과장");
        TransientObject so2 = new TransientObject("김삿갓", "영업부", "과장");
        TransientObject so3 = new TransientObject("암행어", "인사부", "과장");
        oos.writeObject(so1);
        oos.writeObject(so2);
        oos.writeObject(so3);
        oos.close();
        FileInputStream fis = new FileInputStream("serialobject2.dat");
        ObjectInputStream ois = new ObjectInputStream(fis);
        TransientObject rso1 = (TransientObject)ois.readObject();
        TransientObject rso2 = (TransientObject)ois.readObject();
        TransientObject rso3 = (TransientObject)ois.readObject();
        System.out.println(rso1.toString());
        System.out.println(rso2.toString());
        System.out.println(rso3.toString());
        ois.close();
    } //end of main
} //end of TransientObjectMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac TransientObjectMain.java
C:\javasrc\chap12>java TransientObjectMain
홍길동:총무부:null
김삿갓:영업부:null
암행어:인사부:null
***/
───────────────────────────────────────ⓑ

TransientObject 클래스는 Serializable을 구현하였으며, 멤버 변수 title은 transient로 명시하였습니다.

▣ 멤버 변수에 transient의 사용
◈ private transient String title;

직렬화할 때 String title 부분을 제외했기 때문에 역직렬화한 후 객체를 복원했을 때 title 부분이 null로 출력되고 있습니다. 

◈ 홍길동:총무부:null
◈ 김삿갓:영업부:null
◈ 암행어:인사부:null

transient는 복잡한 개념이 아닙니다. 단지 객체를 직렬화하는데 제외하겠다는 의미 이외에는 별다른 뜻은 없습니다. 하지만 프로그램상에서 반드시 저장해야 할 것과 저장하지 말아야 할 것을 구분하는 작업은 전적으로 프로그래머에게 달려 있습니다. 그리고 스태틱 변수는 기본적으로 transient라는 것도 기억해 두시기 바랍니다. 스태틱은 직렬화의 대상에서 자동으로 제외됩니다. 

▣ 직렬화의 조건
◈ 직렬화될 필드는 반드시 non-static, non-transient로 선언되어야만 직렬화할 때 포함된다.

직렬화될 필드는 non-static, non-transient로 선언해야만 직렬화할 때 포함됩니다. 스태틱 변수는 공유 메모리 개념을 가지고 있기 때문에 직렬화에서 제외됩니다.



12.2.4 직렬화되지 않는 경우



라이브러리 차원에서 미리 직렬화가 구현된 클래스들도 있습니다. 그리고 직렬화할 수 없는 클래스들도 존재합니다. 해당 클래스가 직렬화가 가능한지 아닌지를 알기 위해서는 자바 API에서 Serializable 인터페이스가 구현되었는 지를 확인하시면 됩니다. 여러분들이 라이브러리 관련 객체를 직렬화하고 싶다면 자바 API를 참조하셔서, 그 객체가 Serializable 인터페이스를 구현했는지 확인하시고 사용하시길 바랍니다.

직렬화를 구현한다고 해서 모든 클래스들이 직렬화가 가능한 것은 아닙니다. 다음과 같은 경우 직렬화를 할 수 없습니다.

▣ 직렬화가 불가능한 경우
◈ 직렬화가 불가능한 객체를 포함한 경우
◈ 하위 클래스는 직렬화를 구현했지만 상위 클래스에서는 직렬화가 구현되지 않은 상태에서 상위 클래스의 생성자에 매개변수가 있는 경우

특정 클래스의 객체를 포함한 경우 아예 직렬화가 불가능한 경우가 있습니다. 다음은 직렬화할 수 없는 대표적인 클래스들을 보여주고 있습니다. 

▣ 직렬화 불가능한 클래스들
◈ 이벤트 어댑터
◈ 이미지 필터
◈ AWT 클래스
◈ beans
◈ Socket
◈ URLConnection

두 번째의 경우는 하위 클래스는 직렬화를 구현했지만 상위 클래스는 직렬화를 구현하지 않은 상태이며, 그리고 상위 클래스의 생성자에 매개변수가 있는 상태라면 직렬화할 때 문제가 발생할 수 있습니다. 엄밀하게 말하면 역직렬화에 문제가 생길 수 있습니다. 이 경우 직렬화는 가능하지만 역직렬화가 불가능한 상태가 됩니다. 

먼저 직렬화할 수 없는 객체를 포함한 경우부터 보도록 하겠습니다. 다음의 클래스는 Serializable은 구현했지만 멤버 변수로 직렬화 불가능한 Socket 클래스의 객체를 포함하고 있습니다. 그렇기 때문에 직렬화할 때 NotSerializableException이 발생합니다.

『chap12\MyNetwork.java』
ⓙ───────────────────────────────────────
/**
Serializable은 구현했지만 직렬화할 수 없는 객체를 포함한 경우
**/
import java.io.*;
import java.net.*;
public class MyNetwork implements Serializable{
    private Socket socket; //직렬화 불가능한 객체
    public MyNetwork() throws IOException{
        socket = new Socket();
    }
} //end of MyNetwork class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac MyNetwork.java
***/
───────────────────────────────────────ⓑ

『chap12\NotSerialMain.java』
ⓙ───────────────────────────────────────
/**
MyNetwork의 객체를 직렬화할 때 에러발생
**/
import java.io.*;
public class NotSerialMain{
    public static void main(String[] args) 
            throws IOException, ClassNotFoundException{
        MyNetwork m = new MyNetwork();
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bout);
        oos.writeObject(m);
        oos.close();
        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bin);
        Object o = ois.readObject();
    } //end of main
} //end of NotSerialMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac NotSerialMain.java
C:\javasrc\chap12>java NotSerialMain
Exception in thread "main" java.io.NotSerializableException: java.net.Socket
    at ObjectOutputStream.writeObject0(ObjectOutputStream.java:1054)
    at ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1332)
    at ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1304)
    at ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1247)
    at ObjectOutputStream.writeObject0(ObjectOutputStream.java:1052)
    at ObjectOutputStream.writeObject(ObjectOutputStream.java:278)
    at NotSerialMain.main(NotSerialMain.java:10)
***/
───────────────────────────────────────ⓑ

위의 에러는 직렬화가 불가능한 객체를 직렬화하려 했기 때문에 발생한 에러입니다. 이 때 직렬화가 불가능한 객체를 transient로 처리해서 직렬화되는 것을 방지하면 에러를 제거할 수 있습니다.

▣ 직렬화가 불가능한 객체를 transient로 처리한 경우
◈ public class MyNetwork implements Serializable{
◈         private transient Socket socket;
◈         public MyNetwork() throws IOException{...}
◈ } 

직렬화할 수 없기 때문에 transient로 직렬화를 못하게 하는 것입니다. 

두 번째의 경우는 직렬화할 때 상위 클래스와 하위 클래스가 존재할 때, 하위 클래스에서만 직렬화를 구현하였다면 직렬화하지 못하는 경우가 발생합니다. 다음과 같은 경우에 직렬화할 수 없는 상황이 발생합니다.

▣ 상속구조에서 직렬화할 수 없는 상황(엄밀하게 말하면 직렬화는 가능하지만 역직렬화가 불가능한 상태)
◈ 상위 클래스의 생성자에 매개변수가 있는 생성자만 존재
◈ 상위 클래스는 Serializable을 구현하지 않은 상태
◈ 하위 클래스는 상위 클래스를 상속받은 뒤 Serializable을 구현한 상태

이와 같은 상태에서 하위 클래스의 객체를 생성한 후 직렬화시키는 것은 가능합니다. 하지만 역직렬화하는 것이 불가능합니다. 이것은 상위 클래스가 직렬화되지 않았기 때문에 상위 클래스의 정보가 없는 상황이 발생합니다. 상위 클래스를 직렬화하지 않으면 상위 클래스의 정보도 직렬화되지 않기 때문에 무조건 디폴트 생성자를 이용하게 됩니다. 이러한 경우를 예제로 만들어 보죠.

『chap12\SerialConsMain.java』
ⓙ───────────────────────────────────────
/**
상속구조에서 직렬화할 수 없는 상황
**/
import java.io.*;
class Parent extends Object{
    public Parent(String str){
        //...
    }
} //end of Parent class

class Child extends Parent implements Serializable{
   public Child(String str) throws IOException {
       super(str);
   }
} //end of Child class

public class SerialConsMain{
   public static void main(String[] args) 
        throws IOException, ClassNotFoundException{
      Child c = new Child("test");
      ByteArrayOutputStream bout = new ByteArrayOutputStream();
      ObjectOutputStream oout = new ObjectOutputStream(bout);
      oout.writeObject(c);
      oout.close();
      ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
      ObjectInputStream oin = new ObjectInputStream(bin);
      Object o = oin.readObject();
   } //end of main
} //end of SerialConsMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac SerialConsMain.java
C:\javasrc\chap12>java SerialConsMain
Exception in thread "main" java.io.InvalidClassException: 
    Child;    no valid constructor
    at ObjectStreamClass.<init>(ObjectStreamClass.java:379)
    at ObjectStreamClass.lookup(ObjectStreamClass.java:253)
    at ObjectOutputStream.writeObject0(ObjectOutputStream.java:1010)
    at ObjectOutputStream.writeObject(ObjectOutputStream.java:278)
    at SerialConsMain.main(SerialConsMain.java:19)
***/
───────────────────────────────────────ⓑ

위의 예제는 하위 클래스의 객체를 직렬화한 상태에서 다시 역직렬화를 할 때 상위 클래스의 정보가 없기 때문에 역직렬화를 할 수 없는 상황을 예제로 보여주고 있습니다. 위의 문제를 해결하는 것은 아주 간단합니다. 상위 클래스에 implements Serializable을 붙여주면 됩니다. 그렇게 되면 역직렬화할 때 상위 클래스의 정보를 보고 매개변수있는 생성자를 찾아서 역직렬화를 완성하게 되는 것입니다.

▣ 에러 해결책
◈ 상위 클래스에 implements Serializable을 붙여준다.
저작자 표시 비영리 변경 금지

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

12.3 Externalizable  (0) 2010/02/21
12.2 Serializable  (0) 2010/02/21
12.1 Serialization  (0) 2010/02/21
11.3 리플렉션 프로그래밍  (0) 2010/02/21
11.2 정적 바인딩 클래스와 동적 바인딩 클래스  (0) 2010/02/21
11.1 Reflection  (0) 2010/02/21

12.1 Serialization

JAVA PROGRAMMING/JAVA 2010/02/21 22:09 by 킨테리 KinTeL



12.1.1 클래스와 객체의 관계



객체 직렬화를 논하기 전에 우선 클래스와 객체의 관계에 대해서 알아보도록 하죠. 클래스(Class)와 객체(Object)의 관계는 클래스의 기초 개념을 학습하면서 이미 다루었던 내용입니다. 여기서는 약간 다른 측면에서 생각해 보도록 하죠. 

▣ 클래스와 객체의 관계
◈ 클래스의 형정보와 객체의 메모리 사이의 관계

자바 파일을 컴파일한 후 생성된 .class 파일은 클래스의 모든 정보를 담고 있으며, 객체를 생성하기 위해서는 반드시 해당 .class 파일을 로딩해야 합니다. .class 파일은 클래스의 형정보를 담고 있으며, 형정보 없이 객체를 생성하는 방법은 존재하지 않습니다. .class에 포함된 형정보가 로딩되었다면 객체를 생성할 수 있으며 메서드 또한 호출할 수 있습니다. 그렇다면 다음과 같은 질문을 던질 수 있습니다.

▣ 질문
◈ 클래스의 형정보로 만든 객체의 메모리가 어떠한 형태로 되어 있는가?
◈ 메서드를 호출했을 때 객체의 메모리와 형정보가 어떠한 방식으로 동작하는가?

형정보를 이용해서 객체의 메모리를 만들 때 멤버 메서드와는 관계가 없습니다. 일반적으로 멤버 변수의 크기와 객체의 메모리의 크기는 같습니다. 자바에서는 메모리를 직접 접근할 수 없기 때문에 메모리의 크기를 증명할 수는 없지만 ANSI C++에서는 아주 당연한 것입니다. C++에서 객체의 메모리를 계산하기 위해서 sizeof라는 연산자를 제공하고 있습니다. 실제 이것을 테스트하기 위한 간단한 예제를 만들어 보도록 하죠.

『chap12\objtest\objmain.cpp』
ⓙ───────────────────────────────────────
/**
객체의 메모리 크기를 계산하는 예제
**/
#include <stdio.h>
class RefObj{
private:
    int a;
    int b;
public:
    void SetData(int a, int b){
        this->a = a;
        this->b = b;
    }
    void PrintData(){
        printf("a=%d  b=%d\n", a, b);
    }
};
int main(void){
    RefObj r1;
    RefObj r2;
    r1.SetData(100,200);
    r1.PrintData();
    r2.SetData(1000,2000);
    r2.PrintData();
    printf("size of RefObj r1 : %d byte\n", sizeof(r1));
    printf("size of RefObj r2 : %d byte\n", sizeof(r2));
    return 0;
}
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>CL.EXE  objmain.cpp
C:\javasrc\chap12>objmain.exe
a=100  b=200
a=1000  b=2000
size of RefObj r1 : 8 byte
size of RefObj r2 : 8 byte
***/
───────────────────────────────────────ⓑ

▣ 참고
◈ RefObj라는 클래스는 C++의 형식의 클래스이지만 자바의 클래스와 비슷한 모양을 하고 있습니다. C++에서는 메서드의 선언과 구현을 분리시키지만, 최대한 자바와 비슷한 형식으로 구현하기 위해서 인라인(inline) 메서드 형식으로 구현하고 있습니다. 

위의 예에서 RefObj형의 객체의 메모리를 생성하기 위해서 다음과 같이 메모리를 생성하고 있습니다. 

▣ RefObjet형의 객체의 메모리 생성
◈ RefObj r1;
◈ RefObj r2;

표준 C++에서는 클래스형의 변수 선언은 메모리의 생성을 의미합니다. 그렇기 때문에 위의 구문은 객체의 메모리 생성을 의미합니다. 

위의 예에서 sizeof 연산자를 이용해서 RefObj r1과 r2의 메모리를 계산하고 있습니다. 이 때 나타나는 메모리의 크기는 8바이트입니다. 이것은 int형 멤버 변수 2개를 가지고 있기 때문에 RefObj라는 클래스의 메모리는 8바이트가 되는 것입니다. 그렇다면 다음과 같은 결론을 내릴 수 있습니다. 

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

객체의 메모리는 멤버 변수의 메모리와 동일한 크기를 가지게 됩니다. 만약 RefObj r1과 r2를 이용해서 메서드를 호출한다면, r1과 r2의 메모리와 RefObj 클래스의 형정보를 조합해서 메서드를 호출하게 됩니다.

객체의 메모리와 메서드의 호출【chap12\classobject.bmp】
 




메서드의 형태는 클래스의 정보가 있는 부분에 있으며, 객체의 메모리는 메모리 영역에 독립적으로 존재합니다. 클래스의 형정보와 객체의 메모리를 조합해서 메서드를 호출하거나 멤버 필드에 값을 변경할 수 있습니다. 가령 메서드 내에서 멤버 변수가 사용되어진다면, r1의 멤버 변수인지 r2의 멤버 변수인지만 구분해 주면 됩니다.

▣ 참고
◈ 위의 r1과 r2의 메서드가 호출되어지고 메서드 내부에서 멤버가 사용되어질 때, r1의 멤버 변수인지 r2의 멤버 변수인지를 구분하는 방법은 의외로 간단하다. 단순히 메서드를 호출할 때 숨어있는 매개변수가 하나 있다고 생각하면 된다. 이 숨어있는 매개변수가 바로 객체의 참조값이 된다. C++로 생각하면 주소겠지만 자바에서는 참조값을 들고다닌다고 생각하면 쉽게 해결할 수 있다. 실제 메서드에서 사용된 멤버가 어느 객체의 멤버 변수인지는 첨부된 참조값을 이용해서 구분할 수 있다.

클래스의 형정보와 객체의 메모리만 있다면 언제든지 메서드를 호출할 수 있습니다. 이것은 형태와 내용의 절묘한 조합입니다.

위의 그림에서 r1과 r2의 메모리는 독립적으로 존재하면서 RefObj 클래스의 형정보를 공유하는 형식으로 메서드를 호출하게 됩니다. 위의 방식은 C++에서 이용되는 방식이지만 자바에서도 이것은 동일합니다. 형정보와 객체의 메모리는 독립적으로 존재하면서 유기적으로 동작하게 됩니다.

객체의 메모리는 프로그램이 실행되는 동안 유지되는 순간적인 메모리입니다. 이 순간적인 객체의 메모리를 지속적으로 보관할 방법에 대해서 생각해 보도록 하죠. 그렇다면 여러분은 다음과 같은 질문을 던질 것입니다.

▣ 질문
◈ 객체의 어떠한 데이터를 저장할까?

쉽게 생각해 보면 위의 RefObj r1과 r2의 메모리를 저장하면 됩니다. 그리고 다시 복원하면 될 것입니다. 하지만 단순히 r1과 r2의 메모리를 저장한다면 각각 8바이트씩 저장될 것이며, 나중에 복원할 때 8바이트의 데이터가 무엇인지 알 방법이 없습니다. 즉 객체의 메모리와 약간의 정보를 포함시켜서 저장해야 할 것입니다.

▣ 객체 저장
◈ 객체의 메모리 자체와 객체에 대한 정보를 포함시켜야 한다.

객체의 메모리와 객체에 대한 약간의 정보를 포함시켜서 객체를 저장할 수 있다고 가정하죠. 그렇다면 다시 객체로 복원하는 메커니즘이 필요할 것입니다. 객체의 메모리와 정보를 해석해서 원래의 객체로 복원하는 것을 수작업으로 할 수 있지만, 매번 저장하고 복원할 때마다 구현하는 것은 불합리한 방법입니다.

▣ 직렬화(Serialization)
◈ 객체를 저장하는 기법

▣ 역직렬화(Deseialization)
◈ 직렬화된 객체를 복원하는 기법

자바에서는 객체를 저장하고 저장된 객체를 복원하는 작업을 자동화시켰으며, 객체를 저장하는 기법을 직렬화(Serialization)라고 하며 다시 복원하는 작업을 역직렬화(Deseialization)라고 합니다.

우리가 이 장에서 하려고 하는 작업은 가상머신에 존재하는 객체의 메모리 그 자체를 저장하거나, 통째로 네트웍으로 전송하려고 하는 것입니다. 저장을 하든 네트웍으로 전송을 하든 간에, 객체는 일련의 바이트의 형태로 되어 있어야 합니다. 약속된 규칙에 의해서 객체의 메모리를 한 줄로 늘어선 바이트의 형태로 만드는 것을 객체의 직렬화(Serialization)라고 하며, 다시 객체의 형태로 복원하는 작업을 우리는 객체의 역직렬화(Deserialization)라고 합니다.



12.1.2 직렬화의 개념



객체 직렬화란 객체를 바이트로 저장하는 기술을 말합니다. 즉 가상머신 내에 존재하는 특정 객체의 메모리를 바이트의 형태로 변환하는 것을 말합니다. 물론 바이트로 변환된 데이터를 다시 객체로 복원할 수도 있습니다. 객체를 바이트로 변환하는 것을 직렬화(Selializaiton)라고 하며, 바이트로 변환된 것을 다시 복원하는 작업을 역직렬화(Deselialization)라고 합니다. 

▣ 직렬화를 하는 이유
◈ 객체의 메모리는 순간적이기 때문에 영구적으로 보관하기 위해서

직렬화를 하는 이유는 객체의 메모리가 프로그램이 실행되는 동안에 유지되는 순간적인 메모리이기 때문에 이것을 지속적으로 보관하기 위해서입니다. 객체의 메모리에서 필요한 데이터를 저장한 후 다시 복원하는 기술 또한 필요합니다. 이러한 기법은 가상머신 차원에서 제공해 주고 있습니다.

몇 가지 작업만 해주면 가상머신 내부에서 직렬화가 자동으로 이루어집니다. 하지만 여기서는 직렬화의 개념 파악을 위해서 수작업으로 직렬화시키는 방법을 알아본 후 자바에서 이용할 수 있는 직렬화를 학습하도록 하겠습니다. 먼저 직렬화할 클래스부터 작성하도록 하죠.

『chap12\Employee.java』
ⓙ───────────────────────────────────────
/**
직렬화의 대상 클래스
**/
public class Employee {
    private String name;    // 이름
    private String dept;    // 부서
    private String title;    // 직책
    public Employee (String name, String dept, String title) {
        this.name = name;
        this.dept = dept;
        this.title = title;
    }// 생성자
    public String toString(){
        return name + ":" + dept + ":" + title;
    }
    public String getSerialData(){
        String data = "name=" + name + "\r\n";
                data += "dept=" + dept + "\r\n";
                data += "title=" + title;
        return data;
    }
} //end of Employee class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac Employee.java
***/
───────────────────────────────────────ⓑ

Employee라는 클래스의 객체를 다음과 같이 만들었을 때 해당 객체를 구성하는 핵심적인 데이터가 존재할 것입니다.

▣ Employee형의 객체 생성
◈ Employee e = new Employee("홍길동", "총무부", "부장");

Employee e의 멤버 변수에 해당하는 중요한 데이터는 name, dept, title이 될 것입니다. 그렇다면 name, dept, title의 데이터를 저장한다면 다시 객체로 복원할 수도 있을 것입니다. 물론 실제 직렬화에서는 데이터만 저장되는 것이 아니라 형정보도 추가되지만 여기서는 개념적인 직렬화의 예만을 테스트하도록 하겠습니다.

name, dept, title의 데이터를 저장하기 위해서 우선 저장할 규칙을 만들어야 합니다. 저장할 규칙은 다음과 같이 정하도록 하죠. 이 규칙은 여러분이 원하는대로 정하시면 됩니다.

▣ 저장하는 규칙
◈ name=홍길동
◈ dept=총무부
◈ title=부장

저장 규칙의 형태대로 만들기 위해서 Employee 클래스 내에 getSerialData()라는 메서드를 만들어 두었습니다. 

◈ public String getSerialData(){
◈         String data = "name=" + name + "\r\n";
◈                 data += "dept=" + dept + "\r\n";
◈                 data += "title=" + title;
◈         return data;
◈ }

직렬화할 클래스가 준비되었다면 실제 Employee 객체를 생성한 후 객체를 저장하는 클래스를 만들어 보겠습니다.

『chap12\SerialStream.java』
ⓙ───────────────────────────────────────
/**
객체의 직렬화를 담당하는 클래스
**/
import java.io.*;
public class SerialStream{
    private FileWriter fw;
    public SerialStream(FileWriter fw){
        this.fw = fw;
    }
    public void saveObject(Employee e) throws IOException{
        String str = e.getSerialData();
        fw.write(str);
    }
    public void close() throws IOException{
        fw.close();
    }
} //end of SerialStream class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac SerialStream.java
***/
───────────────────────────────────────ⓑ

출력 스트림을 생성자의 매개변수로 준 후 saveObject(Employee e)만 호출하면 객체가 해당 출력 스트림으로 저장될 것입니다. Employee 객체를 저장하는 방법은 다음과 같습니다.

◈ Employee e = new Employee("홍길동", "총무부", "부장");
◈ SerialStream outs = new SerialStream(new FileWriter("serial.dat"));
◈ outs.saveObject(e);
◈ outs.close();

위와 같이 하면 serail.dat 파일에 저장하려고 하는 객체의 정보가 기록될 것입니다. 이제 반대로 serial.dat 파일에 있는 데이터를 읽어온 후 다시 객체로 복원하는 클래스를 만들어 보도록 하죠.

『chap12\DeSerialStream.java』
ⓙ───────────────────────────────────────
/**
객체의 역직렬화를 담당하는 클래스
**/
import java.io.*;
import java.util.*;
public class DeSerialStream{
    private BufferedReader br;
    public DeSerialStream(FileReader fr){
        this.br = new BufferedReader(fr);
    }
    public Employee restoreObject() throws IOException{
        String temp, name=null, dept=null, title=null;
        while((temp = br.readLine()) != null){
            StringTokenizer st = new StringTokenizer(temp, "=");
            String str = st.nextToken();
            if(str != null){
                if(str.equals("name")){
                    name = st.nextToken();
                }else if(str.equals("dept")){
                    dept = st.nextToken();
                }else if(str.equals("title")){
                    title = st.nextToken();
                }else{
                    System.out.println("잘못된 데이터입니다");    
                }
            }
        }
        if( (name!=null) && (dept!=null) && (title!=null)){
            Employee e = new Employee(name, dept, title);
            return e;
        }else{
            return null;
        }
    }
    public void close() throws IOException{
        br.close();
    }
} //end of DeSerialStream class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac DeSerialStream.java
***/
───────────────────────────────────────ⓑ 

입력 스트림을 이용해서 DeSerialStream 객체를 생성한 후 restoreObject()를 호출하면, 스트림에 연결된 파일로부터 데이터를 읽어들인 후 객체로 복원해서 리턴하는 예제입니다. 실제 객체를 복원하는 방법은 다음과 같습니다.

◈ DeSerialStream ins = new DeSerialStream(new FileReader("serial.dat"));
◈ Employee r = ins.restoreObject();
◈ System.out.println("원본 Employee e:" + e + " " + e.hashCode());
◈ System.out.println("복원 Employee e:" + r + " " + r.hashCode());
◈ ins.close();

위의 객체를 저장하고 복원하는 방법을 테스트하는 예는 다음과 같습니다.

『chap12\SerialTest.java』
ⓙ───────────────────────────────────────
/**
객체를 저장하고 복원하는 방법을 테스트하는 예
**/
import java.io.*;
public class SerialTest{
    public static void main(String[] args) throws IOException{
        Employee e = new Employee("홍길동", "총무부", "부장");
        SerialStream outs = new SerialStream(new FileWriter("serial.dat"));
        outs.saveObject(e);
        outs.close();
        DeSerialStream ins = new DeSerialStream(new FileReader("serial.dat"));
        Employee r = ins.restoreObject();
        System.out.println("원본 Employee e:" + e + " " + e.hashCode());
        System.out.println("복원 Employee e:" + r + " " + r.hashCode());
        ins.close();
    } //end of main
} //end of SerialTest class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac SerialTest.java
C:\javasrc\chap12>java SerialTest
원본 Employee e:홍길동:총무부:부장 23671010
복원 Employee e:홍길동:총무부:부장 17332331
***/
───────────────────────────────────────ⓑ




12.1.3 직렬화의 방법



앞에서 배운 수동으로 직렬화하는 방법은 단순히 직렬화의 의미를 파악하기 위해서 테스트한 것이며, 실제 직렬화는 이보다 더 복잡한 과정을 필요로 합니다. 자바의 직렬화는 직렬화의 메커니즘이 내부적으로 완벽하게 감추어져 있기 때문에 객체 직렬화를 직접 구현을 하는 것이 아니라 규칙에 맞게 사용하는 방법을 배우는 것이라고 보면 됩니다. 자바에서 직렬화를 구현하기 위해서는 Serializable이나 Exteranalizable 인터페이스를 이용합니다.

▣ 직렬화를 위한 인터페이스
◈ Serializable 인터페이스
◈ Exteranalizable 인터페이스

객체 스트림에 저장될 객체의 클래스는 반드시 Serializable이나 Externalizable 둘 중 하나를 구현해야 합니다. 이들 중 하나의 인터페이스를 구현함으로써 생성된 객체가 저장(직렬화)될 의사가 있음을 반드시 밝혀야 합니다.

▣ 객체 직렬화를 위한 Serializable 인터페이스의 원형
◈ public interface Serializable {
◈         //...
◈ }

▣ 객체 직렬화를 위한 Externalizable 인터페이스의 원형
◈ public interface Externalizable extends Serializable {
◈         public void writeExternal(ObjectOutput out) throws IOException;
◈         public void readExternal(ObjectInput in)
◈                 throws IOException, ClassNotFoundException;
◈ }

▣ Serializable이나 Externalizable을 구현한다는 의미
◈ Serializable이나 Externalizable을 구현한 클래스로 객체를 생성했다면, 해당 객체가 저장(직렬화)될 의사가 있음을 밝히는 것이다.

Serializable 인터페이스는 어떠한 메서드도 포함하고 있지 않은 표시(Marker) 인터페이스입니다. Serializable 인터페이스를 구현하기 위해서는 단순히 implements Serializable을 붙여만 주면됩니다. 

▣ Serializable 인터페이스의 구현
◈ public class MyObject implements Serializable{
◈         //...
◈ }

Serializable의 표시만 해주면 해당 클래스의 객체는 자동으로 멤버 필드들의 값을 저장하고 복구할 수 있는 능력을 가지게 됩니다. 반면, Externalizable 인터페이스를 구현한 클래스는 저장할 멤버 필드들의 종류와 값을 writeExternal()과 readExternal()을 이용해서 저장하고 복원하는 과정을 직접 구현해야 합니다. 

▣ Serializable 인터페이스
◈ 객체가 직렬화될 수 있다는 것을 나타내는 표시이다. 이 때 데이터의 저장과 복구는 자동으로 이루어진다.
▣ Externalizable 인터페이스
◈ 직렬화의 권한은 있지만 객체저장과 복구의 방법을 사용자가 직접 구현해야 합니다. 즉 프로그래머가 데이터의 저장과 복구를 제어할 수 있다.
저작자 표시 비영리 변경 금지

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

12.3 Externalizable  (0) 2010/02/21
12.2 Serializable  (0) 2010/02/21
12.1 Serialization  (0) 2010/02/21
11.3 리플렉션 프로그래밍  (0) 2010/02/21
11.2 정적 바인딩 클래스와 동적 바인딩 클래스  (0) 2010/02/21
11.1 Reflection  (0) 2010/02/21

11.3 리플렉션 프로그래밍

JAVA PROGRAMMING/JAVA 2010/02/21 22:06 by 킨테리 KinTeL



11.3.1 리플렉션을 위한 준비



동적 바인딩으로 클래스를 로딩한 후에 다운캐스팅할 수 없는 상황에서 객체를 생성하고 메서드를 호출하며 멤버 필드에 접근하는 예를 알아보겠습니다. 앞으로 테스트할 프로그램은 다음과 같습니다.

▣ 리플렉션 기법들
◈ 생성자에 매개변수가 없는 객체 생성하기
◈ 생성자에 매개변수가 있는 객체 생성하기
◈ 매개변수가 없는 메서드 호출하기
◈ 매개변수가 있는 메서드 호출하기
◈ 특정객체에 멤버 필드 값 셋팅하기
◈ 특정객체에 멤버 필드 값 얻어내기

동적으로 클래스를 로딩하는 루틴을 메서드로 만들어 두도록 하죠. 작업의 편리를 위해서 다음과 같이 Class 클래스를 로딩하는 부분을 스태틱 메서드로 만들어 두겠습니다.

『chap11\DynamicLoader.java』
ⓙ───────────────────────────────────────
/**
동적 바인딩으로 클래스를 로딩하기 위한 클래스
**/
import java.net.*;
public class DynamicLoader{
    public static Class loadClass(String url, String className) 
        throws MalformedURLException,InstantiationException,
            IllegalAccessException, ClassNotFoundException{
        URL[] urlArray = {new URL(url)};
        URLClassLoader cLoader = new URLClassLoader(urlArray);
        Class c =cLoader.loadClass(className);
        return c;
    } //end of main
} //end of DynamicLoader class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac DynamicLoader.java
***/
───────────────────────────────────────ⓑ

위의 DynamicLoader는 네트웍의 특정 위치에 있는 .class 파일을 로딩한 후에, Class 클래스의 형태로 리턴하는 기능을 가지고 있습니다. DynamicLoader 클래스를 테스트하는 예는 다음과 같습니다.

『chap11\DynamicLoaderTestMain.java』
ⓙ───────────────────────────────────────
/**
DynamicLoader를 테스트하는 예
**/
import java.net.*;
public class DynamicLoaderTestMain{
    public static void main(String[] args) 
        throws MalformedURLException, InstantiationException,
            IllegalAccessException, ClassNotFoundException{
        String url = "http://www.jabook.org/";
        Class c = DynamicLoader.loadClass(url, "RemoteData");
        System.out.println(c.getName());
    } //end of main
} //end of DynamicLoaderTestMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac DynamicLoaderTestMain.java
C:\javasrc\chap11>java DynamicLoaderTestMain
RemoteData
***/
───────────────────────────────────────ⓑ

위의 예는 DynamicLoader를 이용해서 웹 서버에 존재하는 RemoteData.class를 동적으로 로딩하기 위해서 다음과 같이 loadClass(String url, String className)을 호출하고 있습니다.

▣ Class 클래스를 로딩하기 위한 메서드 테스트
◈ String url = "http://www.jabook.org/";
◈ Class c = DynamicLoader.loadClass(url, "RemoteData");

위의 예제에서 사용된 RemoteData.class는 실제 웹 서버에 존재해야 합니다. RemoteData 클래스의 소스 코드는 다음과 같습니다.

『chap11\RemoteData.java』
ⓙ───────────────────────────────────────
/**
동적 바인딩으로 로딩할 클래스
RemoteData.class는 다른 컴퓨터에 존재해야 한다.
**/
public class RemoteData {
    private String name = "이름없음";
    public String addr = "주소없음";
    public RemoteData(){}
    public RemoteData(String name, String addr){
        this.name = name;
        this.addr = addr;
    }
    public void sayHello(){
        System.out.println(this.name + ":안녕하세요!!");
    }
    public void sayHello(String guest){
        System.out.println(this.name + ":" + guest + "님 안녕하세요!!");
    }
    public void goodBye(){
        System.out.println(this.name + ":안녕계세요^.^");
    }
    public String toString(){
        return super.toString() + ":" + name + ":" + addr;
    }
} //end of RemoteData class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac RemoteData.java
***/
───────────────────────────────────────ⓑ

이제 동적 바인딩으로 클래스를 로딩할 준비가 되었으니 리플렉션 프로그램을 학습하도록 하죠.



11.3.2 객체생성



리플렉션 기법으로 객체를 생성하는 방법부터 학습하도록 하죠. 동적 바인딩으로 클래스를 로딩한 후에 객체를 생성하는 방법에는 두 가지가 있습니다. 

▣ 동적 바인딩 기법으로 객체를 생성하는 방법
◈ 생성자에 매개변수가 없는 객체 생성하기
◈ 생성자에 매개변수가 있는 객체 생성하기

생성자에 매개변수가 없는 경우와 매개변수가 있는 경우의 객체 생성은 약간 차이가 납니다. 매개변수가 없는 경우에는 Class 소속의 newInstance()를 이용합니다. 앞아서도 배웠습니다만 여기서 확인하는 의미에서 다시 작성해 보도록 하죠. 먼저 여러분들은 동적 바인딩 형식으로 Class 클래스를 로딩해야 합니다. 그리고 Class 클래스를 이용해서 newInstance() 메서드를 호출하면 됩니다.

『chap11\CtorRemoteDataMain.java』
ⓙ───────────────────────────────────────
/**
생성자에 매개변수 없는 객체 생성하기
**/
import java.net.*;
public class CtorRemoteDataMain{
    public static void main(String[] args) throws MalformedURLException,
        InstantiationException, IllegalAccessException, ClassNotFoundException{
        String url = "http://www.jabook.org/";
        Class c = DynamicLoader.loadClass(url, "RemoteData");
        Object obj = c.newInstance();
        System.out.println(obj);
    } //end of main
} //end of CtorRemoteDataMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac CtorRemoteDataMain.java
C:\javasrc\chap11>java CtorRemoteDataMain
RemoteData@42719c:이름없음:주소없음
***/
───────────────────────────────────────ⓑ

다른 컴퓨터에 존재하는 .class 파일을 로딩하기 위해서 앞에서 만들어 둔 DynamicLoader를 이용하고 있습니다.

▣ 동적 바인딩으로 생성자에 매개변수가 없는 객체 생성하기
◈ String url = "http://www.jabook.org/";
◈ Class c = DynamicLoader.loadClass(url, "RemoteData");
◈ Object obj = c.newInstance();

이 때 적어도 자신이 로딩할 클래스의 이름은 알고 있어야 합니다. Class 클래스가 로딩되었다면 newInstance()를 이용해서 객체를 생성하면 됩니다. 객체를 생성했다 하더라도 동적 바인딩으로 클래스를 로딩하기 때문에 다운캐스팅은 할 수 없습니다. 여기서는 단순히 객체 생성만을 테스트하고 있습니다.

▣ 리플렉션의 팁
◈ 여러분들이 리플렉션을 사용하는 이유는 전혀 모르는 클래스를 핸들하고자 하는 것이 아니라, 클래스의 모든 정보는 알고 있지만 형변환을 할 수 없기 때문에 리플렉션을 사용하는 것입니다.

두 번째로 생성자에 매개변수가 있는 객체를 생성하도록 하겠습니다. 생성자에 매개변수가 있으면 객체를 생성할 때 Class 소속의 newInstance()를 사용하지 않고, Constructor 소속의 newInstance()를 사용합니다. 먼저 동적 바인딩의 기법으로 Class를 받았다면 클래스 내에 존재하는 생성자 중에서 사용할 생성자를 찾아야 합니다. 

▣ 생성자를 검색하는 이유
◈ 생성자 오버로딩에 의해서 여러 개의 생성자가 존재할 수 있기 때문에 사용하려고 하는 생성자를 찾아야 합니다.

Class 내에 존재하는 생성자를 찾는 방법은 생성자의 매개변수 형태를 이용해서 찾습니다. 현재 여기서 호출하려고 하는 생성자는 다음과 같습니다.

▣ 호출하려고 하는 생성자
◈ public RemoteData(String name, String addr){ 
◈         //...
◈ }

먼저 생성자에 매개변수가 어떤 모양을 하고 있는지, Class 클래스를 이용해서 배열 형태로 만듭니다. 하나의 클래스 내에 생성자가 여러 개 존재할 수 있기 때문에 어떤 생성자를 사용할지 결정하는 것입니다.

◈ Class[] paraType = new Class[] {String.class, String.class};

두 개의 String 매개변수를 가지고 있기 때문에 2개의 String.class가 사용된 것입니다. 매개변수의 형태가 정해졌다면 동적 바인딩 클래스에서 paraType에 해당하는 매개변수를 가진 생성자를 찾으면 됩니다. 이 때 getConstructor() 메서드를 이용해서 생성자를 검색하게 됩니다.

▣ 두 개의 String형을 매개변수로 가진 생성자 찾기
◈ Constructor con = c.getConstructor(paraType);

생성자를 찾았다면 이제 실제 사용될 매개변수를 만들어야 합니다. 실제 매개변수로 사용되는 매개변수 또한 배열 형태로 만들어야 합니다.

▣ 생성자의 매개변수로 사용할 배열
◈ Object[] initPara = new Object[] {"홍길동", "서울"};

모든 것이 준비되었으니 Constructor con의 newInstrance(Object[] initargs)를 이용해서 객체를 생성하면 됩니다. 객체를 생성하는 방법은 다음과 같습니다.

▣ Constructor를 이용한 객체생성
◈ Object obj = con.newInstance(initPara);

생성자에 매개변수 있는 객체를 생성하는 전체 예제는 다음과 같습니다.

『chap11\CtorRemoteDataMain2.java』
ⓙ───────────────────────────────────────
/**
생성자에 매개변수 있는 객체 생성하기
**/
import java.net.*;
import java.lang.reflect.*;
public class CtorRemoteDataMain2 {
    public static void main(String[] arg) 
        throws MalformedURLException, InstantiationException, 
        IllegalAccessException, ClassNotFoundException, 
        NoSuchMethodException, InvocationTargetException{

        String url = "http://www.jabook.org/";
        Class c = DynamicLoader.loadClass(url, "RemoteData");
        Class[] paraType = new Class[] {String.class, String.class};
        Constructor con = c.getConstructor(paraType);
        Object[] initPara = new Object[] {"홍길동", "서울"};
        Object obj = con.newInstance(initPara);
        System.out.println(obj);
    } //end of main
} //end of CtorRemoteDataMain2 class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac CtorRemoteDataMain2.java
C:\javasrc\chap11>java CtorRemoteDataMain2
RemoteData@1e5e2c3:홍길동:서울
***/
───────────────────────────────────────ⓑ

매개변수 없는 생성자의 객체 생성은 Class의 newInstance()를 사용하지만, 매개변수가 있는 생성자의 객체 생성은 Constructor의 newInstrance(Object[] initargs)를 이용합니다.



11.3.3 멤버 필드 접근



Class 클래스를 이용해서 객체를 만드는 방법에 대해서 알아보았습니다. 하지만 다운캐스팅할 수 없기 때문에 멤버 메서드를 호출하거나 멤버 필드에 접근할 수 없었습니다. 리플렉션을 이용하면 형을 복원하지 않은 상태에서 멤버 메서드나 멤버 필드에 접근할 수 있습니다. 먼저 멤버 필드에 접근하는 방법부터 알아보도록 하죠. 

동적 바인딩으로 클래스를 로딩했기 때문에 다음과 같이 다운캐스팅을 하지 못하는 Object형이 존재합니다.

◈ String url = "http://www.jabook.org/";
◈ Class c = DynamicLoader.loadClass(url, "RemoteData");
◈ Object obj =  c.newInstance();

동적 바인딩으로 생성된 객체 Object obj의 멤버 필드에 접근하기 전에 우선적으로 멤버 필드의 명(변수의 이름)은 알고 있어야 합니다. 현재는 addr이라는 public 멤버 필드에 접근한 후 값을 셋팅하려고 합니다. 먼저 동적 바인딩 Class 클래스에서 해당 멤버 필드를 이름(변수의 이름)으로 찾아야 합니다. Class 클래스를 이용해서 멤버 필드를 찾기 위해서 Class 소속의 getField(String name)를 이용하면 됩니다.

▣ addr 멤버 필드 찾기
◈ Field f = c.getField("addr");

Object obj의 멤버 필드 addr에 값을 셋팅하기 위해서 Field f의 set(Object obj, Object value)을 이용합니다.

▣ obj객체의 addr에 값 셋팅하기
◈ f.set(obj, "서울특별시"); 

그리고 다시 Object obj에 존재하는 멤버 필드 addr의 값을 받아내기 위해서는 다음과 같이 get(Object obj)을 이용하면 됩니다.

▣ obj객체의 addr 값 얻어내기
◈ Object x = f.get(obj); 

형을 복원하지 않은 상태에서 클래스들의 정보만으로 멤버 필드에 값을 셋팅하고 다시 얻어내는 전체 예제는 다음과 같습니다.

『chap11\FieldRemoteDataMain.java』
ⓙ───────────────────────────────────────
/**
멤버 필드의 접근을 테스트하는 예
**/
import java.net.*;
import java.lang.reflect.*;
public class FieldRemoteDataMain {
    public static void main(String[] arg) 
        throws MalformedURLException, InstantiationException,
        IllegalAccessException, ClassNotFoundException, 
        NoSuchFieldException{

        String url = "http://www.jabook.org/";
        Class c = DynamicLoader.loadClass(url, "RemoteData");
        Object obj =  c.newInstance();
        Field f = c.getField("addr");
        f.set(obj, "서울특별시");//멤버 필드에 값 셋팅하기
        Object x = f.get(obj); //멤버 필드에 값 얻어내기
        System.out.println(x);
    } //end of main
} //end of FieldRemoteDataMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac FieldRemoteDataMain.java
C:\javasrc\chap11>java FieldRemoteDataMain
서울특별시
***/
───────────────────────────────────────ⓑ



11.3.4 메서드의 호출



멤버 필드에 접근하는 방법에 대해서 알아보았습니다. 이번에는 메서드를 호출하는 방법에 대해서 알아보겠습니다. 메서드는 매개변수가 있을 때와 매개변수가 없을 때 두 가지 측면을 고려해야 합니다.

▣ 동적 바인딩 기법으로 메서드를 호출하는 방법
◈ 매개변수가 없을 때의 메서드 호출
◈ 매개변수가 있을 때의 메서드 호출

당연히 매개변수가 있는 것보다 없는 것이 휠씬 단순합니다. 프로그램 방식은 멤버 필드에 접근하는 방법과 비슷합니다. 우선 동적 바인딩의 기법으로 클래스를 로딩한 후 형을 복원할 수 없는 객체가 있어야겠죠.

◈ String url = "http://www.jabook.org/";
◈ Class c = DynamicLoader.loadClass(url, "RemoteData");
◈ Object obj = c.newInstance();

위의 obj의 멤버 메서드 sayHello()와 goodBye()를 호출하려고 합니다. 먼저 Class 클래스에서 sayHello라는 이름을 검색해서 해당 메서드를 찾아야 합니다. 물론 여러분은 최소한 호출할 메서드의 이름은 알고 있어야 합니다. 메서드를 검색하기 위해서는 Class 클래스 소속의 getMethod(String name, Class[] parameterTypes)를 사용하면 됩니다.

▣ 메서드명으로 Method 검색
◈ Method m = c.getMethod("sayHello", null);
◈ Method m2 = c.getMethod("goodBye", null);

메서드를 찾기 위해 찾을 메서드 이름과 매개변수를 getMethod()의 매개변수로 넣어주면 됩니다. 이 때 매개변수가 없다면 null로 명시하면 됩니다. 

이제 다운캐스팅을 할 수 없는 Object obj의 멤버 메서드를 호출해야겠죠. Method m은 sayHello()를, Method m2는 goodBye()를 담당하는 객체입니다. Method형의 객체를 이용해서 Object obj의 sayHello()와 goodBye() 메서드를 다음과 같이 호출할 수 있습니다.

▣ Method를 이용한 메서드의 호출
◈ m.invoke(obj, null);
◈ m2.invoke(obj, null);

이렇게 하면 객체의 형을 몰라도 메서드를 호출할 수 있습니다. 다음은 매개변수 없는 2개의 메서드를 호출하는 예입니다.

『chap11\MethodRemoteDataMain.java』
ⓙ───────────────────────────────────────
/**
매개변수 없는 메서드 호출하는 예
**/
import java.net.*;
import java.lang.reflect.*;
public class MethodRemoteDataMain {
    public static void main(String[] arg) 
        throws MalformedURLException, InstantiationException,
            IllegalAccessException, ClassNotFoundException, 
            NoSuchMethodException, InvocationTargetException{

        String url = "http://www.jabook.org/";
        Class c = DynamicLoader.loadClass(url, "RemoteData");
        Object obj =  c.newInstance();
        Method m = c.getMethod("sayHello", null);
        m.invoke(obj, null);
        Method m2 = c.getMethod("goodBye", null);
        m2.invoke(obj, null);
    } //end of main
} //end of MethodRemoteDataMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac MethodRemoteDataMain.java
C:\javasrc\chap11>java MethodRemoteDataMain
이름없음:안녕하세요!!
이름없음:안녕계세요^.^
***/
───────────────────────────────────────ⓑ

위의 프로그램에서 null이라고 표시되어 있는 부분이 있습니다. 이것은 매개변수를 넣어주는 자리입니다. 하지만 매개변수가 없는 메서드이기 때문에 null을 사용하고 있습니다. 그렇다면 null 대신에 매개변수를 넣어서 메서드를 호출해 보도록 하죠. 기본적인 메커니즘 똑같습니다. 단지 Class 객체에서 메서드를 찾을 때 null 대신에 메서드의 매개변수의 타입을 배열 형식으로 넣어주는 것이 다릅니다.

메서드명은 sayHello이며 String형의 매개변수를 가진 메서드를 검색하는 방법은 다음과 같습니다.

▣ 동적 바인딩 기법으로 Method 검색하기
◈ String url = "http://www.jabook.org/";
◈ Class c = DynamicLoader.loadClass(url, "RemoteData");
◈ Object obj =  c.newInstance();
◈ Class[] typePara = new Class[] {String.class};
◈ Method m = c.getMethod("sayHello", typePara);

다운캐스팅할 수 없는 Object obj의 sayHello에 매개변수를 넣어서 호출하는 방법은 다음과 같습니다.

▣ Method를 이용한 메서드의 호출
◈ Object[] initPara = new Object[] {"선생"};
◈ m.invoke(obj, initPara);

Method m은 Object obj의 sayHello(String guest)를 대신 호출하는 것입니다. 매개변수가 있는 메서드를 호출하는 전체 소스코드는 다음과 같습니다.

『chap11\MethodRemoteDataMain2.java』
ⓙ───────────────────────────────────────
/**
매개변수 있는 메서드 호출하는 예
**/
import java.net.*;
import java.lang.reflect.*;
public class MethodRemoteDataMain2 {
    public static void main(String[] arg) 
        throws MalformedURLException, InstantiationException,
            IllegalAccessException, ClassNotFoundException, 
            NoSuchMethodException, InvocationTargetException{

        String url = "http://www.jabook.org/";
        Class c = DynamicLoader.loadClass(url, "RemoteData");
        Object obj =  c.newInstance();
        Class[] typePara = new Class[] {String.class};
        Method m = c.getMethod("sayHello", typePara);
        Object[] initPara = new Object[] {"선생"};
        m.invoke(obj, initPara);
    } //end of main
} //end of MethodRemoteDataMain2 class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac MethodRemoteDataMain2.java
C:\javasrc\chap11>java MethodRemoteDataMain2
선생님 안녕하세요!!
***/
───────────────────────────────────────ⓑ




11.3.5 결론



리플렉션에서 소개되는 프로그램 기법은 아주 독특한 면을 가지고 있습니다. 예를 들어 클래스로 메서드를 호출하려고 할 때 호출하려고 하는 메서드의 이름과 매개변수의 형과 개수 정도는 알고 있어야 합니다. 혹자는 이것에 대한 불만을 가지고 있을 수 있습니다. 모르면 전부 모른다는 가정하에서 프로그램을 할 것이지 어떤 것은 알아야 되고 어떤 것은 몰라도 되는 것은 비합리적이라고 말할 것입니다. 이것은 리플렉션의 의미를 잘 몰라서 하는 말일 것입니다.

실제 리플렉션은 클래스의 모든 정보를 알고 있으면서, 다운캐스팅할 수 없는 상태에서 프로그램하는 것이 리플렉션 프로그램의 기본입니다. 어쩔 수 없이 프로그램은 실행된 런타임 상태이며, 런타임 상태에서 방금 만든 따끈따끈한 클래스가 있다고 가정하죠.  그리고 이 클래스를 로딩해서 사용하려고 한다면, 동적으로 클래스를 로딩해서 객체를 만들고 실행해야만 합니다. 결국 방금 만들었기 때문에 클래스의 스펙은 알고 있는 상태이지만, 다운캐스팅할 수 없는 상태에서 프로그램을 하는 것이 됩니다. 이러한 프로그램 기법을 리플렉션(Reflection)이라고 하는 것입니다.
저작자 표시 비영리 변경 금지

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

12.2 Serializable  (0) 2010/02/21
12.1 Serialization  (0) 2010/02/21
11.3 리플렉션 프로그래밍  (0) 2010/02/21
11.2 정적 바인딩 클래스와 동적 바인딩 클래스  (0) 2010/02/21
11.1 Reflection  (0) 2010/02/21
10.6 Class getClass()  (0) 2010/02/21



11.2.1 정적 바인딩 클래스



가상머신이 바이트 코드로 된 .class 파일을 로딩한 후에 객체를 생성할 수 있듯이, 프로그래머가 .class 파일을 직접 로딩한 후 Class 클래스를 이용해서 객체를 생성할 수도 있습니다. 일반적인 방법으로 객체를 생성하면 가상머신이 그 다음 작업을 해주는 것입니다.

▣ 정적 바인딩
◈ 컴파일할 때 해당 .class 파일이 필요하다면, 해당 클래스를 정적 바인딩으로 사용하는 것이다.

▣ Data 클래스
◈ class Data {
◈         //...클래스의 내용
◈ }

▣ 프로그램 내에서 객체 생성
◈ Data d = new Data();

프로그램 내에서 Data 클래스가 사용되었다면 가상머신은 Data.class를 로딩한 뒤 가상머신 차원의 특수한 방법으로 객체를 만들 것입니다. 프로그래머가 직접 이 모든 작업을 흉내낸다면 다음과 같은 절차가 될 것입니다.

▣ Class 클래스를 이용한 객체 생성의 절차
◈ Data.class 파일 로딩
◈ Data.class를 이용해서 객체 생성

프로그래머가 프로그램적으로 이것을 구현하기 위해서는 해당 클래스의 Class를 로딩한 후 newInstance() 메서드를 호출하면 됩니다. 

▣ Class 클래스를 이용한 객체 생성
◈ Class c = Data.class; //정적 바인딩의 기법
◈ Object obj = c.newInstance();

알고 보면 'Data d = new Data()'라는 한줄이 가상머신 차원에서 생각해 보면 두줄의 프로그램으로 나타낼 수 있는 것입니다. 그리고 가상머신에 이미 로딩되어 있는 Data.class를 얻어내는 방법은 getClass()를 이용한다고 배웠습니다. 객체를 생성했다면 이미 Data.class가 로딩된 상태입니다. 이미 로딩된 Data.class는 객체를 통해서 얻어낼 수 있습니다.

▣ 객체로부터 Class 클래스 얻어내기
◈ Data d = new Data(); //정적 바인딩의 기법
◈ Class c = d.getClass();

위의 두 가지 방식의 클래스 사용 방법은 정적인 바인딩의 기법입니다. 정적 바인딩 기법이라는 말은 이미 컴파일할 때 컴파일러가 Data 클래스를 알고 있는 상태입니다. 그렇기 때문에 컴파일할 때 Data.class가 클래스 패스가 지정된 곳에 존재하지 않으면 컴파일 자체가 되지 않습니다. 그리고 이러한 경우 다음과 같이 Data 클래스 자체를 프로그램에서 사용할 수 있습니다.

▣ Data라는 이름을 프로그램적으로 사용하는 경우(정적으로 바인딩되는 경우)
◈ Class c = Data.class;
◈ Object obj = c.newInstance();
◈ Data d = (Data)obj; 
◈ Class c2 = d.getClass();
◈ Data d2 = (Data)c2.newInstance();

컴파일 타임에 이미 Data 클래스가 정적으로 바인딩되어 있기 때문에 Data라는 단어 자체를 프로그램에 사용할 수 있습니다.

『chap11\StaticBindingMain.java』
ⓙ───────────────────────────────────────
/**
정적 바인딩을 이용한 다운캐스팅
**/
class Data{
    public void sayHello(){
        System.out.println("Hello World!");
    }
} //end of Data class

public class StaticBindingMain{
    public static void main(String[] args) 
        throws InstantiationException, IllegalAccessException{
        Class c = Data.class; //정적 바인딩으로 Data 클래스의 사용 I
        Object obj = c.newInstance();
        Data d = (Data)obj; //정적 바인딩으로 Data 클래스의 사용 II
        Class c2 = d.getClass();
        //정적 바인딩으로 Data 클래스의 사용 III
        Data d2 = (Data)c2.newInstance();
        System.out.println(d);
        System.out.println(d2);
        d.sayHello();
        d2.sayHello();
    } //end of main
} //end of StaticBindingMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac StaticBindingMain.java
C:\javasrc\chap11>java StaticBindingMain
Data@18e3e60
Data@1a125f0
Hello World!
Hello World!
***/
───────────────────────────────────────ⓑ

위의 경우에 Data 클래스가 없다면 컴파일 자체가 되지 않습니다. 클래스 패스가 지정된 곳에 Data.class 파일이 존재해야 합니다. 그렇지 않다면 Data.class와 Data라는 단어는 프로그램에서 사용할 수조차 없습니다.

정적으로 바인딩된 경우에는 다운캐스팅을 할 수 있습니다.[중요] 그리고 다운캐스팅을 할 수 없다면 생성된 객체의 메서드를 호출할 수 없습니다. 만일 다음과 같은 경우라면 위의 sayHello()는 호출할 수 없습니다.

▣ 다운캐스팅과 메서드 호출
◈ Class c = Data.class;
◈ Object obj = c.newInstance();
◈ obj.sayHello(); //다운캐스팅하지 않으면 sayHello()는 호출할 수 없다.(오류)
◈ //sayHello()는 Object형에 존재하지 않는 메서드이다.
◈ //Data형으로 다운캐스팅 후 sayHello()를 호출해야 한다.

Object형의 obj로 호출할 수 있는 메서드는 Object 소속의 메서드 뿐입니다. 가상 메서드의 원리를 이용한다 하더라도 Object 소속의 멤버 메서드만을 호출할 수 있습니다.

▣ 참고
◈ 가상 메서드는 상위 클래스의 이름으로 하위 클래스의 메서드를 호출하는 특징이 있다. 이 때 상위 클래스의 이름으로 호출할 수 있는 메서드는 상위 클래스에 존재하는 메서드 뿐이다. 상위 클래스의 메서드가 하위 클래스에서 재정의되었을 때, 재정의된 메서드가 호출되기 때문에 일단은 상위 클래스에 호출하려는 메서드가 존재해야 한다.

▣ 정적 바인딩과 다운캐스팅 여부
◈ 정적 바인딩으로 Class를 생성한 후 객체를 만들었다면 다운캐스팅할 수 있다.

정적 바인딩은 다운캐스팅도 가능하며 마음대로 Data라는 형을 사용할 수 있습니다. 정적인 바인딩에서는 클래스 자체가 존재해야 합니다. 클래스 패스가 지정된 곳에 반드시 클래스가 존재해야만 컴파일이 되기 때문입니다.

▣ 참고
◈ 위의 경우는 같은 파일 내에 Data 클래스가 존재하기 때문에 컴파일하면 같은 디렉토리상에 Data.class가 생성된다.



11.2.2 동적 바인딩 클래스



클래스의 동적 바인딩은 컴파일할 때 해당 클래스가 없어도 됩니다. 말그대로 프로그램이 실행되고 난 뒤에 동적으로 위치를 명시하고, 해당 클래스(.class)를 로딩해서 객체를 만든다든지 메서드를 호출할 수 있습니다. 일반적으로 말하는 리플렉션의 기법은 정적 바인딩 클래스가 아니라 동적 바인딩 클래스를 의미합니다. 동적 바인딩의 기법으로 클래스를 로딩하는 방법은 다음과 같습니다.

▣ 동적 바인딩으로 클래스 로딩
◈ Class c = Class.forName("클래스이름");

클래스의 이름 자체를 컴파일 타임에 사용할 수 없기 때문에 문자열 형태로 클래스의 이름을 사용합니다. 그리고 Class.forName("클래스이름")이 호출되는 바로 그 순간에 클래스를 로딩하겠다는 의미가 됩니다. 

◈ 클래스의 이름을 문자열로 사용하는 이유는 해당 클래스가 컴파일할 때 없기 때문입니다.

이 부분을 보다 정확하게 설명하기 위해서 이와 비슷한 상황을 만들어 보죠. 사용할 클래스는 현재의 작업 디렉토리에 존재하지 않고 전혀 다른 컴퓨터에 존재합니다. 다음의 위치에 DynamicData 클래스가 있다고 가정하죠.

▣ DynamicData.class 파일이 있는 위치
◈ http://www.jabook.org/DynamicData.class

『chap11\DynamicData.java』
ⓙ───────────────────────────────────────
/**
다른 컴퓨터에 존재하는 클래스
**/
public class DynamicData {
    public void sayHello(){
        System.out.println("안녕하세요!!");
    }
    public void goodBye(){
        System.out.println("안녕계세요^.^");
    }
} //end of DynamicData class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac DynamicData.java
***/
───────────────────────────────────────ⓑ

여러분들의 프로그램에서는 웹 서버에 존재하는 DynamicData.class라는 파일을 동적으로 다운로드받아서 클래스를 로딩한 후 객체를 생성하려고 할 것입니다.

▣ 동적 바인딩 클래스를 테스트하기 위한 프로그램 절차
◈ 웹 서버에 존재하는 DynamicData.class 파일 다운로드
◈ DynamicData.class 로딩 후 객체 생성 및 메서드 호출

먼저 웹 서버에 존재하는 DynamicData.class 파일을 다운로드받는 방법부터 알아보죠. 웹 서버에 존재하는 파일에 스트림을 생성하는 방법은 다음과 같습니다.

▣ 웹 서버에 존재하는 파일에 연결된 스트림을 생성하는 방법
◈ URL url = new URL("http://www.jabook.org/DynamicData.class");
◈ InputStream is = url.openStream();

그리고 스트림을 통해서 읽은 데이터를 저장하기 위해서 파일 출력 스트림을 하나 생성하는 방법은 다음과 같습니다.

▣ 저장할 파일 스트림을 생성하는 방법
◈ FileOutputStream fos = new FileOutputStream("DynamicData.class");

입력과 출력 스트림이 생성되었다면 while문을 이용해서 네트웍에 연결된 입력 스트림으로부터 데이터를 읽어내고, 로컬 파일 시스템에 연결된 출력 스트림으로 내보내면 됩니다. 이것을 구현하는 예는 다음과 같습니다.

『chap11\DynamicDataClassMain.java』
ⓙ───────────────────────────────────────
/**
동적 바인딩 클래스를 테스트하는 예
**/
import java.io.*;
import java.net.*;
public class DynamicDataClassMain{
    public static void main(String[] args) throws IOException, 
        InstantiationException, IllegalAccessException, ClassNotFoundException{
        System.out.println("다운로드중....");
        URL url = new URL("http://www.jabook.org/DynamicData.class");
        InputStream is = url.openStream();
        FileOutputStream fos = new FileOutputStream("DynamicData.class");
        int i;
        while((i=is.read()) != -1){
            fos.write(i);
            System.out.print("|");
        }
        fos.close();
        is.close();
        System.out.println("\n다운로드 완료...");
        
        Class c = Class.forName("DynamicData");
        Object obj = c.newInstance();
        System.out.println(obj);
    } //end of main
} //end of DynamicDataClassMain clas
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac DynamicDataClassMain.java
C:\javasrc\chap11>java DynamicDataClassMain
다운로드중....
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||
다운로드 완료...
DynamicData@1fee6fc
***/
───────────────────────────────────────ⓑ

위의 상황은 아주 특수한 상황입니다. DynamicData라는 클래스는 다른 컴퓨터의 웹 서버에 존재합니다. 즉 프로그램을 컴파일하는 컴퓨터에서는 DynamicData.class가 없습니다. 프로그램이 실행된 후에야 비로소 DynamicData.class를 다운로드받게 됩니다. 다운로드된 DynamicData.class를 동적으로 로딩한 후 Class 클래스를 얻기 위해서 Class.forName()를 이용합니다.

◈ Class c = Class.forName("DynamicData");
◈ Object obj = c.newInstance();
◈ System.out.println(obj);

정적 바인딩 클래스로 작업을 하든 동적 바인딩 클래스로 작업을 하든간에 Class 클래스를 얻은 후 객체를 생성하는 것은 동일합니다. 

▣ 정적 바인딩으로 Class 클래스 생성 방법
◈ String str = new String("Hello");
◈ Class c1 = String.class;
◈ Class c2 = new String("Hello").getClass();

▣ 동적 바인딩으로 Class 클래스 생성 방법
◈ Class c3 = Class.forName("java.lang.String");

하지만 정적 바인딩과 동적 바인딩은 엄청난 차이가 있습니다. 사실 이 차이점 때문에 여러 가지 리플렉션 기법들이 등장하는 것입니다. 그 차이는 다음과 같습니다.

▣ [중요]정적 바인딩과 동적 바인딩의 차이
◈ 정적 바인딩은 다운캐스팅을 할 수 있다.
◈ 동적 바인딩은 다운캐스팅을 할 수 없다.
◈ 이유:  컴파일 할때 해당 클래스의 이름을 사용할 수 없기 때문

동적으로 바인딩된 클래스로 객체를 생성했을 때는 다운캐스팅이 불가능합니다. 이것은 당연한 것입니다. 만약 위의 코드에서 생성된 객체를 DynamicData형으로 다운캐스팅한다면 컴파일이 안됩니다.

▣ 다운캐스팅이 불가능한 경우
◈ Class c = Class.forName("DynamicData");
◈ Object obj = c.newInstance();
◈ DynamicData d = (DynamicData)obj; //컴파일할 때는 DynamicData가 없다.

컴파일할 때 DynamicData.class 파일 자체가 다른 컴퓨터에 존재하기 때문에 프로그램적으로 DynamicData라는 단어를 사용할 수 없습니다. 위의 예제에서도 문자열로서의 "DynamicData"는 사용했지만, 프로그램적인 데이터 타입으로서의 DynamicData는 사용하지 않고 있습니다.

다운캐스팅할 수 없다는 것은 DynamicData의 멤버 메서드를 호출할 수 없다는 것을 의미합니다. 그렇다면 다음과 같은 질문을 던질 수 있습니다.

▣ 질문(Question)
◈ 다운캐스팅이 되지 않는다면 Object형으로 계속 남아 있어야 한다. 그렇다면 어떻게 DynamicData의 멤버 메서드를 호출할 수 있을까?

이것이 앞으로 전개될 리플렉션(Reflection) 프로그래밍의 핵심입니다. 리플렉션의 기법을 이용하면 다운캐스팅을 하지 않고도 메서드를 호출할 수 있습니다. 주어진 객체의 형을 다운캐스팅을 할 수 없어도 메서드를 호출할 수 있어야 합니다. 현재 배운 기술로는 원래의 형으로 복원하지 않고 해당 클래스의 메서드를 호출하는 것은 불가능합니다. 물론 Object형 소속의 멤버 메서드 9개 정도는 호출할 수 있습니다. 문제는 DynamicData 클래스의 멤버 메서드를 Object형 상태에서 호출하려고 하는 것입니다.

▣ 리플렉션(Reflection)
◈ 형을 복원할 수 없는 객체가 존재할 때 형확인을 하고, 객체를 생성하고, 멤버 메서드까지 호출할 수 있는 프로그램 기법

이러한 리플렉션의 기법을 이용할 수 있도록 자바에서는 기본 라이브러리를 제공해 주고 있습니다. java.lang.reflect 패키지가 그것입니다. 이 패키지 안에는 실시간에 객체의 능력을 분석한 후 다운캐스팅을 할 수 없는 객체라 할지라도 모든 작업을 할 수 있게 지원해 주는 클래스들이 들어 있습니다.

위에서 구현한 프로그램은 그렇게 만족스러운 프로그램 기법은 아닙니다. 위의 방법과 같이 웹 서버에 존재하는 파일을 다운로드받고, Class.forName("클래스이름")으로 Class 클래스를 생성하는 것도 프로그램적으로는 가능합니다. 보통의 경우 원격지에 존재하는 클래스를 로딩할 때는 URLClassLoader 클래스를 이용합니다. URLClassLoader를 생성하기 위해서는 로딩할 클래스의 위치를 URL 배열 형식으로 매개변수에 넣어주면 됩니다.

▣ URLClassLoader 생성하기
◈ URL[] urlArray = {new URL("http://www.jabook.org/DynamicData.class")};
◈ URLClassLoader cLoader = new URLClassLoader(urlArray);

URLClassloader가 만들어졌다면 다음과 같이 원격지에 존재하는 클래스의 Class 클래스를 네트웍을 통해서 로딩할 수 있습니다.

▣ URLClassLoader를 이용해서 Class 클래스 로딩하기
◈ Class c = cLoader.loadClass("DynamicData");

파일을 다운로드받는 것보다 훨씬 깔금한 방식으로 Class 클래스를 생성할 수 있습니다. 다음은 URLClassLoader를 테스트하는 예제입니다.

『chap11\StandardDynamicDataClassMain.java』
ⓙ───────────────────────────────────────
/**
URLClassLoader를 이용한 Class 클래스 로딩
**/
import java.net.*;
public class StandardDynamicDataClassMain{
    public static void main(String[] args) throws MalformedURLException,
        InstantiationException, IllegalAccessException, ClassNotFoundException{
        URL[] urlArray = {new URL("http://www.jabook.org:80/")};
        URLClassLoader cLoader = new URLClassLoader(urlArray);
        Class c = cLoader.loadClass("DynamicData");
        Object obj = c.newInstance();
        System.out.println(obj);
    } //end of main
} //end of StandardDynamicDataClassMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac StandardDynamicDataClassMain.java
C:\javasrc\chap11>java StandardDynamicDataClassMain
DynamicData@15601ea
***/
───────────────────────────────────────ⓑ

지금까지 동적 바인딩에 대해서 알아보았습니다. 앞으로 나올 프로그램은 당연히 형을 다운캐스팅할 수 없는 상태에서 형을 확인하거나 메서드를 호출하는 방법들을 배우게 될 것입니다. 즉 리플렉션의 기법으로 프로그램하는 것을 배우는 것입니다.




11.2.3 형확인이 필요한 상황



정적 바인딩이든 동적 바인딩 클래스이든 둘 다 형확인이 필요한 경우가 있습니다. 객체가 Object형으로 업캐스팅된 경우는 자주 발생합니다. 객체가 매개변수나 리턴으로 사용되었을 때 이러한 경우는 자주 발생합니다. 다음과 같이 임의의 형으로된 obj가 존재할 경우 아무런 조치없이 무조건 원하는 형으로 캐스팅할 수는 없습니다.

▣ 형확인이 필요한 경우
◈ public void action(Object obj){
◈         Servant d = (Servant)obj;//무조건 원하는 형으로 캐스팅할 수 없다.
◈        //...작업
◈ }

이것은 action()의 매개변수로 넘어오는 Object obj가 Servant형이라는 것을 보장할 수 없기 때문입니다. 먼저 이러한 경우를 예제로 만들어 보죠. 다음과 같은 클래스가 존재할 때 메서드 내에서는 형확인은 필수적입니다.

『chap11\ManagerMain.java』
ⓙ───────────────────────────────────────
/**
action(Object obj)에서 형확인이 필요한 경우
**/
class Servant{
    public void cleaningHome(){
        System.out.println("집안 청소를 합니다.");
    }
    public void washingCar(){
        System.out.println("세차합니다.");
    }
} //end of Servant class

class Manager{
    public void action(Object obj){
        Servant d = (Servant)obj;
        d.cleaningHome();
        d.washingCar();
    }
} //end of Manager class

public class ManagerMain{
    public static void main(String[] args){
        Manager m = new Manager();
        //잘못된 형을 매개변수로 준 경우
        m.action(new Object());
    } //end of main
} //end of ManagerMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac ManagerMain.java
C:\javasrc\chap11>java ManagerMain
Exception in thread "main" java.lang.ClassCastException
        at Manager.action(ManagerMain.java:14)
        at ManagerMain.main(ManagerMain.java:22)
***/
───────────────────────────────────────ⓑ

이 예제의 스토리는 다음과 같습니다. 

▣ 예제의 스토리
◈ 하인(Servant)을 지배인(Manager)에게 맡긴다.
◈ 지배인(Manager)은 하인(Servant)에게 청소(cleaningHome)와 세차(washingCar)를 시킨다(action).

그렇다면 지배인(Manager)에게 하인(Servant)을 맡겨야 할 것입니다. 하지만 다음과 같이 지배인(Manager)에게 전혀 다른 객체를 주고 작업을 시키면(action) 문제가 발생합니다.

▣ 잘못된 형의 객체를 매개변수로 준 경우
◈ Manager m = new Manager();
◈ m.action(new Object()); //잘못된 형을 매개변수로 준 경우
◈ //에러발생

위의 상황이 발생하면 지배인은 일을 시킬(action) 수가 없습니다. 하인에게 일을 시켜야 하는데 전혀 다른 놈이 들어 왔으니 일을 시킬 수 없는 것입니다. 위의 예제는 당연히 실행 타임 에러를 발생합니다.

그렇다면 지배인이 일을 시킬 때 무작정 하인으로 취급하지 말고, 하인인지 아닌지 확인한 후 일을 시키면 될 것입니다. 어떻게 action(Object obj)의 매개변수로 들어오는 obj가 Servant형인지를 보장할 수 있을까요? 이 때 형확인을 위해서 이용하는 것이 바로 instanceof 키워드입니다.

▣ instanceof 키워드의 사용 예
◈ if(obj instanceof Servant){
◈         //작업
◈ }

instanceof의 결과값은 true와 false이며, 타입이 맞다면 true를 다르다면 false의 값을 가지게 됩니다. 위의 프로그램에 형확인하는 부분을 추가하면 다음과 같이 작성할 수 있습니다.

『chap11\ManagerMain2.java』
ⓙ───────────────────────────────────────
/**
action(Object obj)에서 형확인을 추가한 경우
**/
class Servant{
    public void cleaningHome(){
        System.out.println("집안 청소를 합니다.");
    }
    public void washingCar(){
        System.out.println("세차합니다.");
    }
} //end of Servant class

class Manager{
    public void action(Object obj){
        if(obj instanceof Servant){
            System.out.println("당신은 Servant군요!");
            Servant d = (Servant)obj;
            d.cleaningHome();
            d.washingCar();
        }else{
            System.out.println("당신은 Servant가 아니군요!");
        }
    }
} //end of Manager class

public class ManagerMain2{
    public static void main(String[] args){
        Manager m = new Manager();
        m.action(new Object()); //잘못된 형을 매개변수로 준 경우
        m.action(new Servant()); //제대로된 형을 매개변수로 준 경우
    } //end of main
} //end of ManagerMain2 class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap11>javac ManagerMain2.java
C:\javasrc\chap11>java ManagerMain2
당신은 Servant가 아니군요!
당신은 Servant군요!
집안 청소를 합니다.
세차합니다.
***/
───────────────────────────────────────ⓑ

지배인(Manager)이 일을 시킬(action) 때, 하인(Servant)인지 아닌지를 instanceof로 확인한 후에 일을 시킨다면 안전하게 일을 시킬 수 있습니다.

▣ 형확인한 후 Servant형으로 다운캐스팅하는 예
◈ if(obj instanceof Servant){
◈        Servant d = (Servant)obj;
◈        d.cleaningHome();
◈        d.washingCar();
◈ }else{
◈        System.out.println("당신은 Servant가 아니군요!");
◈ }

이와 같은 상황은 자주 발생합니다. 6장의 Vector의 경우 Vector로 데이터를 삽입했을 때 형을 잃어버리게 됩니다. 다시 Vector로부터 데이터를 받아낼 때에는 형을 알 수 없습니다. 

◈ Vector v = new Vector();
◈ v.addElement(new String("Hello"));
◈ v.addElement(new Integater(1"));
◈ v.addElement(new Date());
◈ //v에 1000개 정도를 삽입했다고 가정

위의 예처럼 Vector에 addElement(Objet obj) 메서드에 매개변수를 넣어주는 순간 삽입되는 객체는 매개변수에 할당되면서 형을 잃어버리게 됩니다. 그렇기 때문에 Vector의 데이터를 추출하면 형을 알 수 없는 Object형이 됩니다.

▣ Vector에서 인덱스(Index)가 2인 곳의 객체 추출
◈ Object obj = v.elementAt(2);
◈ //obj의 형이 무엇인지 확인할 필요가 있다.

간단한 형확인을 위해서는 instanceof를 사용합니다. 위와 같은 경우 Object obj의 형확인을 위해서 instanceof를 사용하면 됩니다
저작자 표시 비영리 변경 금지

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

12.1 Serialization  (0) 2010/02/21
11.3 리플렉션 프로그래밍  (0) 2010/02/21
11.2 정적 바인딩 클래스와 동적 바인딩 클래스  (0) 2010/02/21
11.1 Reflection  (0) 2010/02/21
10.6 Class getClass()  (0) 2010/02/21
10.5 wait()와 notify()  (0) 2010/02/21
1 2 3 4 5  ... 12 
BLOG main image
(주)KinTeL 회장 김형기

카테고리

분류 전체보기 (159)
My Name Is KinTeL (3)
농구 인생 (1)
사진 이야기 (13)
미디어 (5)
JAVA PROGRAMMING (105)
C# Programming (2)
FLEX (6)
Database (4)
About Eclipse (1)
참조 - Reference (9)
모든 무료 정보 (2)
좋은글 (2)


Statistics Graph
Total : 73,500
Today : 11
Yesterday : 43

최근에 받은 트랙백