본문 바로가기
자바

[자바] 직렬화(Serialization) - Serializable, ObjectInputStream, ObjectOutputStream, Transient, serialVersionUID

by jeonghaemin 2021. 5. 22.
728x90

직렬화란?

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

객체는 클래스 변수나 메서드가 포함되지 않는 인스턴스 변수의 집합이다. 그렇기 때문에 객체를 저장한다는 것은 인스턴스 변수의 값을 저장한다는 것과 같다.

저장했던 객체를 다시 생성하려면, 객체를 생성하고 저장했던 값을 읽어서 인스턴스 변수에 저장하면 된다.

이러한 과정이 프리미티브 타입의 기본형 변수라면 간단할 것 같지만, 배열이나 참조형 같은 경우에는 간단하지 않을 것 같다.

하지만, ObjectInputStream, ObjectOutputStream을 사용하면 간편하게 객체를 직렬화/역직렬화 할 수 있다.

직렬화, 역직렬화 하기 - ObjectInputStream, ObjectOutputStream

  • OjbectInputStream을 사용하여 직렬화를 할 수 있다.
  • ObjectOutputStream을 사용하여 역직렬화를 할 수 있다.

ObjectInputStream, ObjectOutputStream 둘 다 모두 보조스트림이기때문에 입출력할 스트림을 지정해주어야 한다.

//직렬화 예
FileOutputStream fos = new FileOutputStream("member.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeObject(new Member("kim", 25));

//역직렬화 예
FileInputStream fis = new FileInputStream("member.ser");
ObjectInputStream ois = new ObjectInputStream(fis);

Member member = (Member) ois.readObject();

여러 가지 타입의 값을 입출력할 수 있는 메서드를 제공한다.

defaultReadObject, defaultWriteObject 메서드는 현재 클래스의 인스턴스 변수(non-static, non-transient)를 자동으로 직렬화를 수행한다.

직렬화가 가능한 클래스 만들기 - Serializable

java.io.serializable 인터페이스를 구현하면 직렬화가 가능한 클래스를 만들 수 있다.

public class Member implements Serializable {

    String name;
    int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Serializable 인터페이스는 아무 내용이 없는 인터페이스지만, 직렬화가 가능하다는 의미를 식별하는 역할만 한다.

package java.io;

public interface Serializable {
}

부모 클래스에서 Serializable 인터페이스를 구현했다면, 자식 클래스에서는 구현하지 않아도 된다.

class SuperMember implements Serializable {
    ... 
}

public class Member extends SuperMember {
    ...
}

Member 객체를 직렬화하면 부모인 SuperMember 클래스의 인스턴스 변수까지 직렬화가 된다.

자식 클래스에서만 Serializable 인터페이스를 구현하고 부모 클래스에서는 구현하지 않으면, 부모 클래스의 인스턴스 변수는 직렬화 대상에 포함되지 않는다.

class SuperMember {
    String name; //직렬화 대상에 포함되지 않는다.
}

public class Member extends SuperMember implements Serializable {
    ...
}

이 경우 writeObject, readObejct 메서드를 만들어 조상 인스턴스 멤버들을 직접 직렬화를 시킬 수 있다.

  • witeObject, readObject 두 메서드는 직렬화, 역직렬화 작업 시 자동으로 호출된다.
  • 접근 제어자는 private -> 단순히 미리 정해진 규칙
class SuperMember {
    String name;
    int age;
}

public class Member extends SuperMember implements Serializable{
    String address;

    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.writeUTF(name);
        oos.writeInt(age);
        oos.defaultWriteObject();
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        name = ois.readUTF();
        age = ois.readInt();
        ois.defaultReadObject();
    }
}

직렬화 대상에서 제외시키기 - transient

직렬화하려는 객체의 클래스에 직렬화가 안되거나, 비밀번호와 같이 보안상 직렬화되면 안 되는 값은 transient 제어자를 사용해서 직렬화 대상에서 제외시킬 수 있다.

transient가 붙은 인스턴스 변수의 값은 해당 타입의 기본값으로 직렬화된다.

public class Member implements Serializable{

    transient String name; //null로 직렬화 된다.
    transient int age; //0으로 직렬화 된다.
}

직렬화 클래스의 버전 관리 - serialVersionUID

  • 직렬화된 객체를 역직렬화 할 때는 같은 클래스를 사용해야 한다.
  • 같은 클래스더라도 객체를 직렬화 할 때와 역직렬화 할때 클래스의 내용에 변경이 있다면, 아래와 같이 직렬화 할때와 역직렬화 할때 버전이 같아야 되는데 다르다는 예외가 발생한다.
Exception in thread "main" java.io.InvalidClassException: local class incompatible: stream classdesc serialVersionUID = -1491161089619426255, local class serialVersionUID = -2762816076390684970

객체가 직렬화될 때 클래스에 정의된 멤버들의 정보를 이용해서 serialVersionUID라는 클래스의 버전을 자동 생성해서 직렬화 내용에 포함시키고, 역직렬화 할 때 이 클래스의 버전이 같은지 비교함으로써 클래스의 내용이 변경이 있는지 확인한다.

그런데 static 변수나 transient 제어자가 붙은 인스턴스 변수가 추가되는 경우에는 직렬화에 영향을 미치지 않기 때문에 다른 버전으로 인식하게 할 필요가 없다. 이와 같은 상황에 serialVersionUID를 직접 정의하여 클래스의 버전을 수동으로 관리할 수 있다.

클래스 내에 serialVersionUID를 정의하면 클래스의 내용이 변경돼도 클래스의 버전이 자동 생성된 값으로 변경되지 않는다.

public class Member implements Serializable {

    static final long serialVersionUID = 35187314485L;

       ...

}

참고

  • 자바의 정석 3판

댓글