본문 바로가기

Java

자바의 직렬화 (Serialization)

728x90

직렬화란, 객체를 데이터 스트림으로 만드는 것입니다. 객체에 저장된 데이터를 스트림에 쓰기(write) 위해 연속적인(serial) 데이터로 변환하는 것입니다.

반대로, 스트림으로부터 데이터를 읽어서 객체를 만드는 것역직렬화(deserialization)라고 합니다.

 

객체를 저장한다는 것은, 객체의 모든 인스턴스변수의 값을 저장한다는 것을 의미합니다. 그리고, 저장했던 객체를 다시 생성하려면, 객체를 생성한 후에 저장했던 값을 읽어서 생성한 객체의 인스턴스변수에 저장하면 됩니다.

 

인스턴스변수가 단순한 기본형일 때는 인스턴스변수의 값을 저장하는 일은 간단합니다. 하지만, 인스턴스변수의 타입이 참조형일 때는 그냥 저장할 수가 없습니다. 이러한 변수를 저장하기 위해서 자바에서는 ObjectInputStream/ObjectOutputStream을 통해 직렬화/역직렬화를 할 수 있도록 해 줍니다.

 

이번 포스팅은 ObjectInputStream과 ObjectOutputStream을 이용한 자바의 직렬화에 관해서 작성하였습니다.

 

자유로운 피드백은 환영입니다!!


ObjectInputStream과 ObjectOutputStream

직렬화(객체 -> 스트림)에는 ObjectInputStream을, 역직렬화(스트림 -> 객체)에는 ObjectOutputStream을 사용합니다. 사실상 이들은 기반스트림을 필요로 하는 보조스트림 중 하나입니다. 따라서 객체를 생성할 때 입출력(직렬화/역직렬화)할 스트림을 지정해 주어야 합니다. 

ObjectInputStream(InputStream in)
ObjectOutputStream(OutputStream out)

아래 코드는 FileOutputStream을 기반스트림으로 하여 ObjectOutputStream을 생성하는 코드입니다.


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

out.writeObject(new UserInfo());

 

이렇게 코드를 작성하면, objectfile.ser 파일에 UserInfo객체를 직렬화하여 저장합니다. 


역직렬화는 아래와 같이 입력스트림을 이용하고 writeObject(Object object) 대신, readObject()를 사용하여 반환합니다. 이때, readObject()의 반환타입이 Object이므로 객체 원래의 타입으로 형변환해 주어야 합니다.


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

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

 


직렬화 가능한 클래스

위의 UserInfo와 같이, 직렬화가 적용되는 클래스는 Serializable인터페이스를 구현해야 합니다. 즉, 특정 클래스를 직렬화 가능하도록 변경하려면 아래와 같이 Serializable인터페이스를 구현하도록 변경하면 됩니다.


import java.io.Serializable;

public class UserInfo implements Serializable {
        String name;
        String password;
        int age;
}

 

 Serializable인터페이스는 아무 내용이 없는 빈 인터페이스이지만, 직렬화를 고려하여 작성한 클래스인지를 판단하는 기준이 됩니다.


자손클래스의 직렬화

아래와 같이 Serializable을 상속한 클래스의 자손 클래스일 경우, 조상/자손 클래스의 모든 인스턴스변수들이 직렬화 대상이 됩니다.


public class UserInfo implements Serializable {
        String name;
        String password;
}

public class ChildUserInfo extends UserInfo{
    int age;
}

 

 

하지만, 조상 클래스가 아닌 자손 클래스만 Serializable을 구현한다면, 자손 클래스의 인스턴스변수들만 직렬화 대상이 됩니다. 아래의 경우, age 변수만 직렬화되어 저장 가능합니다.


public class UserInfo{
        String name;
        String password;
}

public class ChildUserInfo extends UserInfo implements Serializable{
    int age;
}

 


transient

만약, Serializable를 구현한 클래스 내에 직렬화가 불가능한 클래스 객체가 있다면 Serializable를 구현한 클래스도 직렬화에 실패하게 됩니다. 즉 아래와 같이 Object 클래스는 Seralizable을 구현하지 않기 때문에 직렬화할 수 없어 UserInfo는 직렬화할 수 없습니다.


import java.io.Serializable;

public class UserInfo implements Serializable {
        String name;
        String password;
        int age;
        
        Object obj = new Object(); // Object 직렬화 불가
}

 

 

이런 경우, 즉 직렬화하고자 하는 객체의 클래스에 직렬화가 안 되는 객체에 대한 참조를 포함하고 있다면, 제어자 transient를 붙여서 직렬화대상에서 제외하도록 할 수 있습니다. transient가 붙은 인스턴스변수의 값은 그 타입의 기본값으로 직렬화된다고 할 수 있습니다.


import java.io.Serializable;

public class UserInfo implements Serializable {
        String name;
        transient String password; // 기본값 null로 저장된다.
        int age;
        
        transient Object obj = new Object(); // 기본값 null로 저장된다.
}

    


직렬화를 이용해서 전체 코드로 작성해 보겠습니다.

 

직렬화 대상 클래스


@AllArgsConstructor
public class UserInfo implements Serializable {
    String name;
    String password;
    int age;
    
    public String toString() {
        return "(" + name + ", " + password + ", " + age + ")";
    }
}

 

직렬화


public class Main {
    public static void main(String[] args) {
     try{
         String fileName = "UserInfo.ser";
         FileOutputStream fos = new FileOutputStream(fileName);
         BufferedOutputStream bos = new BufferedOutputStream(fos);

         ObjectOutputStream out = new ObjectOutputStream(bos);

         UserInfo u1 = new UserInfo("User1", "12", 20);
         UserInfo u2 = new UserInfo("User2", "34", 25);

         ArrayList<UserInfo> list = new ArrayList<>();
         list.add(u1);
         list.add(u2);

         out.writeObject(u1);
         out.writeObject(u2);
         out.writeObject(list);
         out.close();

     } catch (IOException e) {
         e.printStackTrace();
     }
    }
}

 

역직렬화


public class Main {
    public static void main(String[] args) {
		try {
            String fileName = "UserInfo.ser";
            FileInputStream fis = new FileInputStream(fileName);
            BufferedInputStream bis = new BufferedInputStream(fis);

            ObjectInputStream in = new ObjectInputStream(bis);

	    // 객체를 읽을 때 (역직렬화 시)
            // 출력(직렬화) 순서와 동일해야 한다!!
            UserInfo u1 = (UserInfo) in.readObject();
            UserInfo u2 = (UserInfo) in.readObject();
            ArrayList list = (ArrayList) in.readObject();

            System.out.println(u1);
            System.out.println(u2);
            System.out.println(list);

            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 


 

만약, 앞서 살펴본 바와 같이 조상 클래스가 직렬화 대상이 아니고, 자손 클래스만 직렬화 가능할 때는 ObjectInputStream과 ObjectOutputStream의 readObject(), writeObject() 메서드를 재구성하여 조상으로부터 상속받은 인스턴스변수도 직접 직렬화되도록 작성해야 합니다. 


public class UserInfo {
    String name;
    String password;

    public UserInfo(String name, String password) {
        this.name = name;
        this.password = password;
    }
} 

public class ChildUserInfo extends UserInfo implements Serializable {
    int age;

    public ChildUserInfo(String name, String password) {
        super(name, password);
        this.age = age;
    }
    
    public String toString(){
         return "(" + name + ", " + password + ", " + age + ")";
    }
    
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeUTF(name); // String 형 --> writeUTF()
        out.writeUTF(password); // 이 외에도 writeInt(), writeLong() 등 다양한 메서드 존재
        out.defaultWriteObject(); // ChildUserInfo에 정의된 인스턴스변수(age) 직렬화 수행
    }
    
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        name = in.readUTF(); // String 형 --> readUTF()
        password = in.readUTF(); // 이 외에도 readInt(), readLong() 등 다양한 메서드 존재
        in.defaultReadObject(); //ChildUserInfo에 정의된 인스턴스변수(age) 역직렬화 수행
    }
}

[참고자료]

남궁 성, [Java의 정석 3rd Edition], 도우출판, 2016

 

728x90
반응형