[JAVA] 직렬화(Serializable)는 왜 하는 것일까?

Mando·2023년 4월 16일
1

JAVA

목록 보기
10/10

Java의 직렬화(Serialize)


데이터의 메모리 구조

  • primitive type : integer, float, charactor
  • wrapper type : 메모리 주소 -> 주소값을 따라가면 값 형식 데이터를 참조하게 됨
    (하지만 인스턴스는 해당 프로세스의 메모리 상에서만 유효한 주소를 갖는 데이터)

이 중에서 전송 가능한 데이터는 당연히 primitive type만 가능하다.

wrapper type같은 경우는 수신자입장에서 메모리 주소를 받아도 송신자 입장에서는 무의미한 값이다. 왜냐하면 서로 물리적으로 사용중인 메모리 공간이 일치하지 않기 때문이다.

따라서 주소값의 실체를 다 끌고와서 primitive type 데이터로 변조하는 작업을 거친다.
그렇기 때문에 직렬화가 된 데이터는 wrapper type은 없고 모든것이 primitive type이다.
그렇기 때문에 네트워크 전송 시 유의미한 데이터가 된다.

결국 직렬화는 패킷 전송시에 유의미한 데이터를 만들기 위해 사용한다.

직렬화(Serialize)

JVM의 Runtime Data Area에 상주하고 있는 개체 데이터를 외부 시스템에서도 사용할 수 있도록 바이트(byte)형태로 데이터를 변환하는 기술

역직렬화

직렬화된 byte 형태의 데이터를 객체로 변환해서 JVM으로 상주시키는 것

Serializable interface에 대해 알아보자

Serializable 인터페이스는 marker interface(메소드가 없는) 인터페이스이다.
Serializable interface를 implements하면 JVM에게 직렬화 준비된 객체라는 것을 알려준다.

public interface Serializable {
}

SerialDto는 직렬화 대상이다. -> 따라서 Serializable interface를 implements했다.

package serializableTest;

import java.io.Serializable;

public class SerialDto implements Serializable {
    private String booName;
    private int bookOrder;
    private boolean bestSeller;

    public SerialDto(String booName, int bookOrder, boolean bestSeller) {
        this.booName = booName;
        this.bookOrder = bookOrder;
        this.bestSeller = bestSeller;
    }

    @Override
    public String toString() {
        return "SerialDto{" +
                "booName='" + booName + '\'' +
                ", bookOrder=" + bookOrder +
                ", bestSeller=" + bestSeller +
                '}';
    }
}

-직렬화

package serializableTest;

import java.io.*;

public class ManageObject {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String filePath = "/Users/suminkim/javaStudy/genericStudy/src/serializableTest/test.md";

        SerialDto dto = new SerialDto("God of Java", 1, true);
        ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(filePath)));
        out.writeObject(dto);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(filePath)));
        SerialDto serialDto = (SerialDto) in.readObject();
        in.close();
        System.out.println(serialDto);
    }
}

-역직렬화

public class ManageObject {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String filePath = "/Users/suminkim/javaStudy/genericStudy/src/serializableTest/test.md";

        ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(filePath)));
        SerialDto serialDto = (SerialDto) in.readObject();
        in.close();
        System.out.println(serialDto);
    }
}

-역직렬화가 제대로 되었는지 확인하기

SerialDto{booName='God of Java', bookOrder=1, bestSeller=true}

serialVersionUID

serialVersionUID는 선언하지 않으면 JVM에서 자동으로 생성해준다.
그러나 명시하는 것을 권장사항으로 하고 있다.

serialVersionUID를 명시하지 않았을 경우 -> 컴파일러가 생성해줌

그 이유는 class가 update될 경우 serialVersionUID가 변경되는데
그 경우 저장되어 있는 UID와 객체의 UID가 다르기 때문에 InvalidClassException을 발생시킨다.

serialVersionUID가 변경될 때 발생하는 에러 확인

SerialDto의 필드를 변경하였다.
이후 파일에는 SerialDto 필드를 변경하기 전 직렬화한 값이 들어있다.
만약, SerialDto 필드를 변경한 이후 이 직렬화 한 값을 역직렬화하는 게 가능할까?

Exception in thread "main" java.io.InvalidClassException: serializableTest.SerialDto; local class incompatible: stream classdesc serialVersionUID = 4451796364825230878, local class serialVersionUID = -4866281420189271189
	at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:728)
	at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2062)
	at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1909)
	at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2235)
	at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1744)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:514)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
	at serializableTest.ManageObject.main(ManageObject.java:15)

객체 형태가 변경되면 컴파일시 serialVersionUID가 다시 생성되기 때문에 역직렬화 시에 예외가 발생한다.

package serializableTest;

import java.io.Serializable;

public class SerialDto implements Serializable {
    private static final long serialVersionUID =1L;
    transient private String booName;
    private int bookOrder;
//    private boolean bestSeller;

    public SerialDto(String booName, int bookOrder) {
        this.booName = booName;
        this.bookOrder = bookOrder;
//        this.bestSeller = bestSeller;
    }

    @Override
    public String toString() {
        return "SerialDto{" +
                "booName='" + booName + '\'' +
                ", bookOrder=" + bookOrder +
//                ", bestSeller=" + bestSeller +
                '}';
    }
}
package serializableTest;

import java.io.*;

public class ManageObject {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String filePath = "/Users/suminkim/javaStudy/genericStudy/src/serializableTest/test.md";

//        SerialDto dto = new SerialDto( "God of Java", 1,true);
//        ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(filePath)));
//        out.writeObject(dto);
//        out.close();

        ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(filePath)));
        SerialDto serialDto = (SerialDto) in.readObject();
        in.close();
        System.out.println(serialDto);
    }
}
SerialDto{booName='null', bookOrder=1}

이처럼 serialVersionUID은 필수는 아니지만
개발자가 serialVersionUID 값을 직접 관리해주면 멤버 변수 및 메서드 추가시 에러가 발생하지 않는다.

trasient 키워드를 이용하여 직렬화 대상에서 제외하기

외부에 노출시키고 싶지 않은 정보의 경우 trasient 키워드를 이용해 직렬화 대상에서 제외시킬 수 있다. trasient가 붙은 인스턴스 변수의 값은 그 타입의 기본값으로 직렬화 된다.

  • primitive type : 각 타입의 default 값
  • reference type : null

ex) booName에 trasient 키워드를 추가하였다.

package serializableTest;

import java.io.Serializable;

public class SerialDto implements Serializable {
    transient private String booName;
    private int bookOrder;
    private boolean bestSeller;

    public SerialDto(String booName, int bookOrder, boolean bestSeller) {
        this.booName = booName;
        this.bookOrder = bookOrder;
        this.bestSeller = bestSeller;
    }

    @Override
    public String toString() {
        return "SerialDto{" +
                "booName='" + booName + '\'' +
                ", bookOrder=" + bookOrder +
                ", bestSeller=" + bestSeller +
                '}';
    }
}

다시 직렬화/역직렬화를 해보고 역직렬화가 된 값을 살펴보자

package serializableTest;

import java.io.*;

public class ManageObject {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String filePath = "/Users/suminkim/javaStudy/genericStudy/src/serializableTest/test.md";

        SerialDto dto = new SerialDto("God of Java", 1,true);
        ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(filePath)));
        out.writeObject(dto);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(filePath)));
        SerialDto serialDto = (SerialDto) in.readObject();
        in.close();
        System.out.println(serialDto);
    }
}

분명히 SerialDto의 booname에 "God of Java"라는 값을 넣어 객체를 생성하였고
해당 객체를 직렬화하였지만, 역직렬화된 값에는 booName에 해당하는 값이 null로 들어있다.

SerialDto{booName='null', bookOrder=1, bestSeller=true}

출처

사진출처

0개의 댓글