JAVA 직렬화

2022. 4. 5. 16:50자바

자바는 메모리에 생성된 객체를 파일 또는 네트워크로 출력할 수가 있다.

객체는 문자가 아니기 때문에 바이트 기반 스트림으로 출력해야 한다. 객체를 출력하기 위해서는

객체의 데이터(필드값)를 일렬로 늘어선 연속적인 바이트로 변경해야 하는데, 

이를 직렬화(serialization)이라고 한다. 

 

반대로 파일에 저장되어 있거나 네트워크에서 전송된 객체를 읽을 수도 있는데, 

입력 스트림으로부터 읽어들인 연속적인 바이트를 객체로 복원하는 것을 역직렬화(deserialization)이라 한다.

 

 

 

package java.io;

public interface Serializable {
}

Serializable의 인터페이스를 보면 메소드가 하나도 없는 것을 볼 수 있다. 

아무런 구현해야 할 메소드도 없는 이 인터페이스가 왜 있을까?

개발을 하다 보면 아래와 같은 경우가 존재한다.

  • 생성한 객체를 파일로 저장
  • 저장한 객체를 읽음
  • 다른 서버에서 생성한 객체를 받음 

이럴 때 꼭 필요한 것이 Serializable 입니다.

우리가 만든 클래스가 파일에 읽거나 쓸 수 있도록 하거나,

다른 서버로 보내거나 받을 수 있도록 하려면 반드시 이 인터페이스를 구현해야 합니다.

 

 

 

Serializable

자바는 Serializable 인터페이스를 구현한 클래스만 직렬화 할 수 있도록 제한한다.

package java.io;

public interface Serializable {
}

io패키지에 있는 인터페이스로서 메소드와 필드가 없다. 

단지, 객체를 직렬화 해도 좋다는 표시 역할을 한다. 

 

객체를 직렬화하면 바이트로 변환되는 것은 필드들이고, 생서자 및 메소드는 직렬화에 포함되지 않는다.

따라서 역직렬화 할 때에는 필드의 값만 복원된다.

모든 필드가 직렬화 되는 건 아니고 static 또는 transient 키워드가 붙은 필드는 직렬화 대상에서 제외된다.

Serializable을 상속받지 않는 객체를 직렬화하면  NotSerializableException이 발생한다.

 

※transient: 직렬화에서 제외하는 용도로 쓰이는 키워드. 다른곳에서는 쓰이지 않는다.

 

 

serailVersionUID 필드

serialVersionUID는 같은 직렬화됐던 객체와 역질렬화 하려는 객체가 같은 클래스임을 알려주는 식별자 역할을 한다.

IDE의 코드상에는 보이지 않지만,  Serializable 상속한 클래스를 컴파일하면 자동적으로

serialVersionUID 정적필드가 추가된다. 

 

문제는 serialVersionUID는 컴파일 할 때마다 값이 달라진다는 것이다.  

(개발환경에 따라 코드내용에 변경이 없이 재 컴파일 하는 경우는 serialVersionUID 값이 안 변할 수도 있다.

 그럴 때는 코드 내용을 변경 하고  컴파일 해보자.   )

 

 

다음의 예제에서 한번에 수정 없이 ObjectOutputEx, ObjectIntputEx를 순차적으로 실행한다면  

아무 문제없이 실행될 것이다.

ClassA  (직렬화,역직렬화 될 클래스)

import java.io.Serializable;

public  class ClassA implements Serializable {
    int field1;
}

 

 

 

ObjectOutputEx

import java.io.*;

public class ObjectOutputEx {
    public static void main(String[] args) throws Exception {
        FileOutputStream fos = new FileOutputStream("C:/workspace/temp.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        ClassA classAOriginal = new ClassA();
        classAOriginal.field1 = 1;

        oos.writeObject(classAOriginal);

        oos.flush();
        oos.close();
        fos.close();
    }
}

실행 후 다음과 같이 파일이 생긴다

 

 

 

 

 

 

ObjectInputEx

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class ObjectInputEx {

    public static void main(String[] args)  throws  Exception{

        FileInputStream fis= new FileInputStream("C:/workspace/temp.txt");
        ObjectInputStream ois=new ObjectInputStream(fis);
        ClassA classAFromFile=(ClassA) ois.readObject();
        System.out.println("classAFromFile.field = " + classAFromFile.field1);

    }
}

실행결과 콘솔에 다음과 같이 출력된다.

 

---------------------------------------------------------------------------------------------------------------------------------------------

 

현재 직렬화 된 객체(ClassA)가 파일에 저장되어있다. (C:/workspace/temp.txt) 

이번엔 ClassA를 다음과 같이 변경해보자.

ClassA.

import java.io.Serializable;

public  class ClassA implements Serializable {
    int field1;
    int field2;
}

filed2가 추가되었다.

 

 

 

 

이 상태에서 ObjectInputEx를 실행시켜보면 다음과 같은 에러가 발생한다.
Exception in thread "main" java.io.InvalidClassException: ClassA; local class incompatible: stream classdesc serialVersionUID = 3718886090764968835, local class serialVersionUID = -1041639892687008082
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2001)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1848)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2158)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
	at ObjectInputEx.main(ObjectInputEx.java:10)

이 에러는 ObjectOutputEx에서 처음 직렬화 할 때(파일에 저장되어 있는 객체)

serialVersionUID는 3718886090764968835 인데 

ClassA 변경 이후에 ClassA의 serialVersionUID의 값은 -1041639892687008082 로 

그 값이 서로 달라서 생기는 에러이다. 

 

 

 

 

 

 

이번엔 처음 직렬화 된 serailVersionUID값인 3718886090764968835  을 ClassA에 적어주자. 

(이 값은 사용자에 따라 다를 수 있다)

ClassA

import java.io.Serializable;
public  class ClassA implements Serializable {
static  final  long serialVersionUID=3718886090764968835L;
    int field1;
    int field2;
}

 

이러면 이 ClassA의 serialVersionUID값은 3718886090764968835로 고정된다.

이 후 다시 ObjectInputEx를 실행하면 

이번에는 serialVersionUID값이 서로 같기 때문에 정상적으로 실행된다.

 

 

이처럼 직렬화 대상 클래스(ClassA)가 변경 될 가능성이 있다면 serailVersionUID를 명시적으로 선언해주면

그 값이 고정되서  재 컴파일 된 이후에도 여전히 역질렬화가 가능합니다. 

 

※직렬화 하는  프로그램과 역질렬화 프로그램이 항상 같지 않을 수 있습니다.  

여러대의 서버가 있는 경우  서버 A에서 직렬화해서 만들어 놓은 파일을 서버B에서 역직렬화 할 수도 있습니다.  

이럴 때는 꼭 serailVersionUID를 명시해줘야 합니다.

 

 

 

추가  출처 : https://techblog.woowahan.com/2551/

 

  • 특별한 문제없으면 자바 직렬화 버전 serialVersionUID의 값은 개발 시 직접 관리해야 합니다.
  • serialVersionUID의 값이 동일하면 멤버 변수 및 메서드 추가는 크게 문제가 없습니다.
    그리고 멤버 변수 제거 및 이름 변경은 오류는 발생하지 않지만 데이터는 누락됩니다.
  • 역직렬화 대상의 클래스의 멤버 변수 타입 변경을 지양해야 합니다. 자바 역직렬화시에 타입에 엄격합니다.
    나중에라도 타입 변경이 되면 직렬화된 데이터가 존재하는 상태라면 발생할 예외를 경우의 수를 다 신경 써야 합니다.
  • 외부(DB, 캐시 서버, NoSQL 서버 등)에 장기간 저장될 정보는 자바 직렬화 사용을 지양해야 합니다.
    역직렬화 대상의 클래스가 언제 변경이 일어날지 모르는 환경에서 긴 시간 동안 외부에 존재했던 직렬화된 데이터는 쓰레기(Garbage)가 될 가능성이 높습니다.
    언제 예외가 발생할지 모르는 지뢰 시스템이 될 수도 있습니다.
  • 개발자가 직접 컨트롤이 가능한 클래스의 객체가 아닌 클래스의 객체에 대해서는 직렬화를 지양해야 합니다.
    개발자가 직접 컨트롤이 힘든 객체 란 보통 프레임워크 또는 라이브러리에서 제공하는 클래스의 객체를 이야기합니다. (사실 직접 변경 가능한 방법은 있지만 추천하진 않습니다.)
    그런 객체가 직접 serialVersionUID를 가지고 있기도 합니다. 그래서 개발 시에 편의상 직렬화 시켜 DB 또는 캐시 서버에 바로 저장하기도 하는데 이 부분에서 많은 문제가 발생합니다.
    • 예시)
      1. 프레임워크 또는 라이브러리가 버전업을 하면서 serialVersionUID 을 변경
      2. 테스트시에는 발생 안 하다가 운영에 반영
    생각 지도 못한 오류가 거품처럼 나기 시작할 것입니다. 이 부분은 사실 알아채기가 힘듭니다. 발생하기 위한 사전 조건도 많기 때문입니다.
    차라리 이 글을 읽으신 분은 위와 같은 문제 사전에 차단하실 것을 추천합니다.
    위와 관련된 예시는 스프링 시큐리티 SecurityContextImpl클래스가 있습니다.
    SecurityContext 를 구현한 클래스 클래스로 링크 를 보면 확인할 수 있습니다.
    serialVersionUID 값이 스프링 시큐리티의 버전 값이기 때문에 버전이 변경될 때마다 신경 쓰입니다.
  • 결론
    • 자바 직렬화를 사용할 때에는 될 수 있으면 자주 변경되는 클래스의 객체는 사용 안 하는 것이 좋습니다.
      변경에 취약하기 때문에 생각지도 못한 예외사항들이 발생할 가능성이 높습니다.
      특히 역직렬 화가 되지 않을 때와 같은 예외처리는 기본적으로 해두는 것을 추천합니다.
      물론 직렬화 대상 클래스 변경과 같은 문제는 자바 직렬 화만 일어나는 문제는 아닙니다만
      자바 직렬화 기술은 중간에 끼어들 여지가 없는 블랙박스에 가까워서 변경 부분에 취약한 문제가 존재합니다.