직렬화(Serialization)

최준호·2021년 12월 9일
0

java

목록 보기
22/25

직렬화(Serialization)

객체를 컴퓨터에 저장했다가 다음에 다시 꺼내어 쓰거나 네트워크를 통해 컴퓨터간 객체를 서로 주고 받을 수 없을까? 그것을 가능하게 해주는 것이 직렬화(Serialization)이다.

직렬화란?

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

객체를 저장하거나 전소앟려면 당연히 이렇게 할 수 밖에 없다. 객체는 클래스에 정의된 인스턴스 변수의 집합니다. 객체에는 클래스 변수나 메서드가 포함되지 않고 오직 인스턴스 변수들로만 구성되어 있다. 인스턴스 변수는 인스턴스마다 다른 값을 가질 수 있어야하기 때문에 별도의 메모리 공간이 필요하지만 메서드는 변하는 것이 아니라 메모리를 낭비해가면서 인스턴스마다 같은 내용의 코드를 포함시킬 이유는 없다.

그래서 객체를 저장한다는 것은 객체의 모든 인스턴스 변수의 값을 저장한다는 것과 같은 의미이다. 인스턴스 변수가 기본형일 때는 간단한 일이지만, 참조형일 경우 간단하지 않다. 인스턴스 변수의 타입이 배열이라면 배열에 저장된 값들도 모두 저장되어야 할것이다. 하지만 우리는 고민하지 않아도 된다. 객체를 직렬화 해주는 ObjectInpuStream과 ObjectOutputStream의 사용법만 알면 된다.

ObjectInputStream과 ObjectOutputStream

직렬화에는 ObjectOutputStream을 사용하고 역직렬화에는 ObejctInputStream을 사용한다. 각각 OutputStream과 InputStream을 상속받지만 기반스트림을 필요로 하는 보조 스트림이다. 그래서 객체를 생상할때 입출력할 스트림을 지정해주어야 한다.

ObjectInputStream(InputStream in)
ObjectOutputStream(OutputStream out)

직렬화가 가능한 클래스 만들기 (Serializable, transient)

직렬화가 가능한 클래스를 만드는 방법은 직력화하고자 하는 클래스가 Serializable 인터페이스를 구현하도록 하면 된다.

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

Serializable 인터페이스는 아무런 내용이 정의되어 있지 않은 빈 인터페이스이지만, 직렬화를 고려하여 작성한 클래스인지 판단하는 기준이 된다.

또한 조상 class가 Serializable을 구현하고 해당 class를 상속 받았다면 자식 class 또한 직렬화가 가능하다.

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

그러나 위의 조상 class인 SuperUserInfo class가 직렬화를 구현하지 않고 자식 class인 UserInfo에서만 직렬화를 구현했다면 조상 class에 정의된 name과 password는 직렬화 대상에서 제외된다.

조상 클래스에 정의된 인스턴스 변수 name과 password를 직렬화 대상에 포함시키기 위해서는 조상 클래스가 직렬화를 구현하도록 하던가, UserInfo에서 조상의 인스턴스변수를 직렬화되도록 처리하는 코드를 직접 추가해야한다.

public class UserInfo implements Serializable{
    String name;
    String password;
    int age;
    
    Object obj = new Object();	//Object 객체는 직렬화할 수 없다.
}

모든 클래스의 최고 조상인 Object는 Serializable을 구현하고 있지 않았기 때문에 직렬화할 수 없다. 인스턴스 변수 obj의 타입이 직렬화가 안되는 Object이긴 하지만 실제로 저장된 객체는 직렬화 가능한 기본 타입 객체라면 직렬화가 가능하다.

public class UserInfo implements Serializable{
    String name;
    String password;
    int age;
    
    Object obj = new String("1234");	//변수 타입은 Object지만 실제 저장되는 객체는 String
}

인스턴스 변수의 타입이 아닌 실제로 연결된 객체의 종류에 의해서 직렬화가 가능한지 아닌지가 결정된다는 것을 기억하자!

transient

직렬화하고자 하는 객체의 클래스에 직렬화가 안되는 객체에 대한 참조를 포함하고 있다면, 혹은 보안상 password같은 직렬화되면 안되는 값에 대해서는 제어자 transient를 붙여서 직렬화 대상에서 제외하도록 할 수 있다.

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

해당 클래스를 직렬화한 후 역직렬화하여 내용을 확인하면 password의 값은 null로 역직렬화되어진다.

실제 직렬화와 역직렬화 코드 작성

예제를 위해 먼저 UserInfo.java 파일을 작성하자

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

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

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", age=" + age +
                '}';
    }
}

그리고 직렬화를 실제로 실행하는 코드를 작성하자

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("man", "1234", 28);
        UserInfo u2 = new UserInfo("woman", "4321", 25);

        ArrayList<UserInfo> list = new ArrayList<>();
        list.add(u1);
        list.add(u2);
        
        out.writeObject(u1);
        out.writeObject(u2);
        out.writeObject(list);
        //직렬화 스트림의 close 호출로 상위 스트림들 모두 flush 및 close
        out.close();
        System.out.println("직렬화 종료");
    }catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

실행 결과

실제로 실행이 잘되며 파일을 생성한다.

이제 다시 역직렬화를 통해 파일을 읽어보자.

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 = " + u1);
        System.out.println("u2 = " + u2);
        System.out.println("list = " + list);
        in.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

실행 결과

toString()으로 정의해놓은 형식으로 출력되며 모든 객체를 읽어온 것을 확인할 수 있다.

직렬화되지 않은 조상 클래스 자식 클래스에서 직렬화하기

class SuperUserInfo{
    String name;
    String password;

    public SuperUserInfo(String name, String password) {
        this.name = name;
        this.password = password;
    }
}
public class UserInfo extends SuperUserInfo implements Serializable {
    int age;

    public UserInfo(String name, String password) {
        super(name, password);
    }

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

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", age=" + age +
                '}';
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeUTF(name);
        out.writeUTF(password);
        out.defaultWriteObject();
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
        name = in.readUTF();
        password = in.readUTF();
        in.defaultReadObject();
    }
}

조상 클래스에서 직렬화하지 않은 경우 자식 클래스에서 다음과 같이 writeObject()readObject()를 직접 추가하여 작성하면 된다. 이 메서드들은 직렬화, 역직렬화 작업시 자동 호출된다. 그리고 두 메서드의 접근 제어자가 private라는 것이 의아할 수 있지만 단순히 정해진 규칙이라 그대로 따르기만 하면 된다.

직렬화 클래스의 버전 관리

직렬화된 객체를 역직렬화할 때는 직렬화 했을 때와 같은 클래스를 사용해야한다. 그러나 클래스의 이름이 같더라도 내용이 변경된 경우 역직렬화는 실패하며 InvalidClassException 이 발생한다.

객체가 직렬화될 때 클래스에 정의된 멤버들의 정보를 이용해서 serialVersionUID라는 클래스 버전을 자동생성하여 직렬화 내용에 포함하는데 역직렬화 할 때 클래스 버전을 비교함으로써 직렬화할 때 클래스의 버전과 일치하는지 확인한다. 그래서 내용이 변경되면 버전이 변경되므로 exception이 발생하는 것이다. 그러나 static 변수나 상수 또는 transient가 붙은 인스턴스 변수는 변경되어도 직렬화에 영향을 미치지 않으므로 상관은 없다.

네트웍으로 직렬화 객체를 전송하는 경우, 보내는 쪽과 받는 쪽이 모두 같은 버전의 클래스를 가지고 있어야하는데 클래스 내용이 조금만 변경되어도 해당 클래스를 재배포해야하는 어려움이 생긴다. 이럴 때는 클래스의 버전을 수동으로 관리해줄 필요가 있다.

class Test implements Serializable{
    static final long serialVersionUIID = 3125661234231231555L;	//정수값이면 어떤 값이든 가능
}

다음과 같이 serialVersionUID를 정해주면 클래스 내용이 바뀌어도 클래스 버전이 자동생성되지 않고 고정된 값으로 지정된다. 정수값이면 아무 값이나 넣어도 상관은 없지만 보통 java에서 serialver.exe를 사용해서 생성된 값을 사용하는 것이 보통이다.

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글