업데이트:

자바 직렬화를 “자바 객체 및 데이터를 다른 외부의 자바 시스템에서 사용하기 위해 byte로 변환하는 기술”로 알고는 있지만 좀 더 상세하게 알고 싶어서 이번 포스트를 작성하게 되었습니다.

자바 직렬화


자바의 직렬화/역직렬화란?

위에서 말했듯이 직렬화는 “자바의 객체 및 데이터를 byte로 변환하는 기술”이고, 역직렬화는 반대로 “byte를 객체나 데이터 형태로 변환하는 기술”입니다. 시스템 적으로는 JVMRuntime Data Area(Heap 또는 Stack)에 있는 객체를 byte로 변환하는 것인데, 좀 더 명확하게 알고 싶어 구글링을 좀 더 해봤습니다.

왜 필요할까?

지금부터 이해를 돕기 위해 Java Object를 기준으로 설명하겠습니다.

Java ObjectJVM > Runtime Data Area > Heap 영역에 존재하게 되는데 이를 참조하기 위해서 주소값을 가지고 참조합니다.

그런데 이 System AobjA주소값을 System B로 보낸다면 어떻게 될까요?

당연히 System B에선 해당 주소값에 objA가 존재하지 않을 것입니다. 그래서 byte 형태로 데이터를 순수 데이터로 변환하여 보내기 위해 직렬화가 필요합니다.

반대로 System B에선 받은 byteobjA로 변환하기 위해 역직렬화가 필요한 것이구요.

NonSerializableException

자바에선 java.io.Serializable 인터페이스를 구현하면 기본 자바 라이브러리를 사용하여 직렬화/역직렬화가 가능합니다.

그리고 직렬화를 하려는 객체에 있는 멤버 변수에 있는 객체도 Serializable가 구현되어 있어야 직렬화가 됩니다. 만약 구현되어 있지 않다면 NonSerializableException가 발생할 것입니다.

다음 예제 코드를 보도록 하겠습니다.

먼저 Member 클래스입니다.

public class Member implements Serializable {

    private long id;
    private String name;
    private Address address;
    private String description;

    public Member(long id, String name, Address address, String description) {
        this.id = id;
        this.name = name;
        this.address = address;
        this.description = description;
    }
}

다음 멤버 변수로 있는 Address 클래스입니다.

public class Address {

    private String basic;
    private String detail;
    private String zipcode;

    public Address(String basic, String detail, String zipcode) {
        this.basic = basic;
        this.detail = detail;
        this.zipcode = zipcode;
    }
}

Member 클래스를 직렬화해보면 다음과 같이 NonSerializableException가 발생하는걸 확인할 수 있습니다.

public class SerializeTest {

    @Test
    void 직렬화_테스트() throws IOException {
        Address address = new Address("서울시 XX구 OO로 1길 2", "101호", "123456");
        Member member = new Member(1L, "시민", address, "직렬화 테스트용 회원입니다.");

        serializeByte(member);

    }

    private byte[] serializeByte(Member member) throws IOException {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
                oos.writeObject(member);
                return baos.toByteArray();
            }
        }
    }
}

NonSerializableException

transient

이때 Address 클래스를 Serializable 구현하도록 하거나 멤버 변수인 addresstransient 키워드를 붙이면 해결 됩니다.

transient는 직렬화 대상에서 제외하는 키워드입니다.

테스트 코드를 통해 직렬화/역직렬화에서 transient가 잘 동작하는지 보겠습니다.

먼저 Member 클래스에 transient를 추가합니다.

@ToString
public class Member implements Serializable {

    private long id;
    private String name;
    private transient Address address; // transient 추가
    private String description;

    public Member(long id, String name, Address address, String description) {
        this.id = id;
        this.name = name;
        this.address = address;
        this.description = description;
    }
}

다음은 직렬화/역직렬화 테스트 코드입니다.

@Test
void 직렬화_역직렬화_테스트() throws IOException, ClassNotFoundException {
    Address address = new Address("서울시 XX구 OO로 1길 2", "101호", "123456");
    Member member = new Member(1L, "시민", address, "직렬화 테스트용 회원입니다.");

    byte[] serializeByte = serializeByte(member);

    Member deserializedMember = deserializeByte(serializeByte);

    System.out.println("deserializedMember = " + deserializedMember);
}

밑에 결과를 보면 다음과 같이 transient로 선언한 addressnull로 출력되는것을 볼 수 있습니다.

deserialize

serialVersionUID


자바 직렬화에 대해 공부하신 분들이라면 serialVersionUID에 대해 보신적이 있으실 텐데요.

그냥 간단히 변수명을 봤을땐 “직렬화 버전에 대한 UID”라는 뜻인데, 이게 무슨 말일까요?

Serializable를 구현하는 경우 직렬화/역직렬화 시 서로의 매핑 정보를 확인하기 위해 serialVersionUID를 비교하는데 이때 명시적으로 serialVersionUID가 존재하지 않는다면 컴파일러가 Class에 대해 해시값을 계산하여 부여합니다.

당연히 클래스 정보가 변경되면 serialVersionUID가 맞지 않게 되고, InvalidClassException이 발생하게 됩니다.

InvalidClassException

InvalidClassException를 확인하기 위한 테스트 코드입니다.

순서는 다음과 같습니다.

  1. 먼저 변환한 byteString 형태로 하기 위해 base64로 인코딩합니다.
    • 위의 member 객체를 byte로 변환, base64로 인코딩하면 다음과 같은 문자열이 나옵니다.

        "rO0ABXNyACBjb20uY2l0aXplbi5zZXR0ZXIuZG9tYWluLk1lbWJlctdPnIAc7VdsAgADSgACaWRMAAtkZXNjcmlwdGlvbnQAEkxqYXZhL2xhbmcvU3RyaW5nO0wABG5hbWVxAH4AAXhwAAAAAAAAAAF0ACfsp4HroKztmZQg7YWM7Iqk7Yq47JqpIO2ajOybkOyeheuLiOuLpC50AAbsi5zrr7w="
      
  2. 클래스 정보를 변경합니다.
    • 멤버 변수중 description을 제거합니다.

        public class Member implements Serializable {
              
            private long id;
            private String name;
            private transient Address address;
              
            public Member(long id, String name, Address address) {
                this.id = id;
                this.name = name;
                this.address = address;
            }
        }
      
  3. base64를 역직렬화를 해봅니다.

     public class SerializeTest {
        
         @Test
         void Serialize_UID_테스트() throws IOException, ClassNotFoundException {
             String base64 = "rO0ABXNyACBjb20uY2l0aXplbi5zZXR0ZXIuZG9tYWluLk1lbWJlctdPnIAc7VdsAgADSgACaWRMAAtkZXNjcmlwdGlvbnQAEkxqYXZhL2xhbmcvU3RyaW5nO0wABG5hbWVxAH4AAXhwAAAAAAAAAAF0ACfsp4HroKztmZQg7YWM7Iqk7Yq47JqpIO2ajOybkOyeheuLiOuLpC50AAbsi5zrr7w=";
             deserializeByte(getDecodeBase64Byte(base64));
         }
        
         private Member deserializeByte(byte[] bytes) throws IOException, ClassNotFoundException {
             try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes)) {
                 try (ObjectInputStream ois = new ObjectInputStream(bais)) {
                     // 역직렬화된 Member 객체를 읽어온다.
                     Object objectMember = ois.readObject();
                     return (Member) objectMember;
                 }
             }
         }
        
         private byte[] getDecodeBase64Byte(String base64) {
             return Base64.getDecoder().decode(base64);
         }
     }
    
  4. InvalidClassException가 발생하는지 확인합니다.
    • 위와 같이 역직렬화를 하면 serialVersionUID가 다르다면서 InvalidClassException가 발생하는 것을 확인할 수 있습니다.

      InvalidClassException

명시적인 serialVersionUID

그럼 컴파일러에게 맡기지 말고 명시적으로 클래스에 serialVersionUID를 추가하면 어떻게 될까요?

Member 클래스에 serialVersionUID1L로 추가하고 위와 똑같은 테스트를 진행해보겠습니다.

public class Member implements Serializable {

    private static final long serialVersionUID = 1L; // serialVersionUID 추가

    private long id;
    private String name;
    private transient Address address;
    private String description;

    public Member(long id, String name, Address address, String description) {
        this.id = id;
        this.name = name;
        this.address = address;
        this.description = description;
    }
}

클래스 변경 전

description 제거 전 테스트를 확인해보면

serialVersionUID_test1

클래스 변경 후

description 제거 후 테스트를 확인해보면

serialVersionUID_test1

정상적으로 역직렬화가 되는걸 확인할 수 있습니다.

serialVersionUID 제약

하지만 serialVersionUID을 맞춘다고 하더라도 클래스 변환에 대한 제약이 있습니다.

멤버 변수 타입의 변경

예를 들어 위의 Member 클래스에 nameString에서 StringBuilder로 변경한다면 ClassCastException가 발생하게 됩니다.

또, idlong에서 int로 변경하게 된다면 InvalidClassException가 발생하게 됩니다.

마무리


지금까지 자바 직렬화/역직렬화에 관한 내용을 알아보았습니다.

결론부터 말하자면 “내가 사용할일이 있을까?”입니다.

계속해서 변경하는 비즈니스에 관련된 클래스에는 적용할 수 없을 뿐더러 다른 곳에 적용하더라도 항상 역직렬화에 대해 Exception이 발생할 것을 생각하고 개발해야 하기 때문에 가져다주는 큰 장점이 없다면 제가 직접 자바 직렬화/역직렬화를 사용할 일은 없어보입니다.

특히나 요새는 Jackson이나 Gson을 사용하여 Json으로 변경하여 데이터를 주고 받는 경우가 많기 때문에 더욱이 사용할 일이 없지 않을까 싶습니다.

혹시나 더 공부하다가 사용할 곳이 발견된다면 그때 다른 포스트로 다뤄보도록 하겠습니다.

오늘도 제 글을 읽어주셔서 감사합니다.

공부하시는 분들에게 도움이 되었으면 좋겠습니다.😀

📌참고


Java Serialization 개념 정리

자바 직렬화, 그것이 알고싶다. 실무편

댓글남기기