[Java] 직렬화(Serialization)와 역직렬화(Deserialization)

파이 ఇ·2024년 5월 31일
1
post-thumbnail

📌 직렬화(Serialization)와 역직렬화(Deserialization)

  • 직렬화 : 객체들의 데이터를 연속적인 데이터(스트림)로 변형하여 전송 가능한 형태를 만드는것.
    • 객체 데이터를 통신하기 쉬운 포맷(Byte, CSV, Json..) 형태로 만들어주는 작업을 직렬화라고 볼 수 있습니다.
  • 역직렬화 : 직렬화된 데이터를 다시 객체의 형태로 만드는 것
    • 역으로 포맷(Byte, CSV, Json..) 형태에서 객체로 변환하는 과정을 역직렬화라고 할 수 있습니다.
public class Person {
	private String name;
    
    public Person(String name) {
    	this.name = name;
	}
}

위와 같은 클래스가 있다고 가정할 때, Json 데이터 형식을 예로 들면
Person person = new Person("김철수"); 객체를 Json 형식인 {"name" : "김철수"}로 변경하는 것을 직렬화, {"name" : "김철수"} 데이터를 받아서 Person이라는 객체의 name필드에 "김철수"를 할당하고 객체를 생성하는 것을 역직렬화라고 할 수 있습니다.

직렬화는 왜 필요할까?

자바에는 Primitive Type이 byte, int, char등 총 8가지가 있습니다. 그리고 그 외 객체(Reference Type : String, Integer..등)들은 주소값을 갖는 참조형 타입입니다.

Primitive Type은 stack에서 값 그 자체를 가지고 있어 외부로 데이터를 전달할 때 값을 일정한 형식의 raw byte 형태로 변경하여 전달할 수 있습니다.

하지만 위 그림과 같이 Reference Type 객체의 경우 실제로 Heap 영역에 존재하고, 스택에서는 Heap 영역에 존재하고 있는 객체의 주소(메모리 주소)를 가지고 있습니다.

위 주소값을 그대로 다른 곳에 보낸다고 가정한다면 ❓
먼저 프로그램이 종료되거나 객체가 쓸모없다고 판단되면 Heap 영역에 있던 데이터는 제거되고, 따라서 본인 메모리에서도 데이터가 사라지게 됩니다.
외부로 전송했다고 가정했을때도, 전송받은 기기의 메모리 주소에 내가 전송하려고 했던 데이터는 존재할 리가 없습니다.

따라서 이 주소값의 데이터(실체)를 Primitive한 값 형식 데이터로 변환하는 작업을 거친 후, 전달해야 합니다. 그렇게 해야 파일 저장이나 네트워크 전송시 Parsing할 수 있는 유의미한 데이터가 됩니다.

🔑 자바에서 직렬화를 구현하는 방법

자바의 primitive type과 java.io.Serializable 인터페이스를 상속받은 객체는 직렬화 할 수 있는 기본 조건을 가지게 됩니다.

public class Person implements Serializable {
	private String name;
	
    public Person(String name) {
    	this.name = name;
	}
    
    // Getter 생략 . .
    
    @Override
    public String toString() {
    	return String.format("Person", name);
	}
}

직렬화 하는 방법은 java.io.ObjectOutputStream 객체를 이용합니다.

Person person = new Person("김철수");
byte serializedPerson[];
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
	try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
    	oos.writeObject(person);
        // serializedPerson -> 직렬화된 person 객체
        serializedPerson = baos.toByteArray();
	}
}
// 바이트 배열로 생성된 직렬화 데이터를 base64로 변환
System.out.println(Base64.getEncoder().encodeToString(serializedPerson));

위 예제는 객체를 직렬화하여 바이트 배열(byte[]) 형태로 변환해보았습니다.

🔑 자바에서 역직렬화를 구현하는 방법

역직렬화를 구현해보기 전에 역직렬화의 조건에 대해 먼저 알아보겠습니다.

  • 직렬화 대상이 된 객체의 클래스가 클래스 패스에 존재해야하며 import 가 되어있어야 합니다.
    • 중요한 점은 직렬화와 역직렬화를 진행하는 시스템이 서로 다를 수 있다는 것을 반드시 고려해야 합니다.
      (같은 시스템 내부라도 소스 버전이 다를 수 있습니다.)
  • 직렬화 대상 객체는 동일한 serialVersionUID를 가지고 있어야 합니다.
private static final long serialVersionUID = 1L;

하지만 Person 객체를 직렬화할 땐 serialVersionUID를 설정하지 않았는데 ? serialVersionUID가 뭔데 ?

이 얘기는 역직렬화를 구현한 뒤 다시 설명하겠습니다.

역직렬화 예제를 살펴보겠습니다.

// 직렬화 예제에서 생성된 base64 데이터
String base64Person = "..생략";
byte serializedPerson = Base64.getDecoder().decode(base64Person);
try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedPerson)) {
	try(ObjectInputStream ois = new ObjectInputStream(bais)) {
    	// 역직렬화된 Person 객체를 읽어옵니다.
	    Object objectPerson = ois.readObject();
    	Person person = (Person) objectPerson;
	    System.out.println(person);
    }
}

java.io.ObjectInputStream 객체를 이용해 역직렬화를 해줄 수 있습니다.

+ serialVersionUID?

serialVersionUID는 무엇인지 예제를 통해 바로 알아보겠습니다.

먼저 기존의 Person 객체를 직렬화 시켜보겠습니다.

Base64.getEncoder().encodeToString(serializedPerson);
rO0ABXNyABp3b293YWhhbi5ibG9nLLkAABSQADYWdlSQAEYWdlM2ltQGJhZW1pbi5jb210AAnquYDrsLDrr7w=

이 문자열을 바로 역직렬화 시키면 Person 객체로 변환됩니다.
하지만 Person 클래스에 나이를 추가해야 하는걸 깜빡 잊어버리고 만들어둔 Person 객체에 얼른 나이 필드를 추가합니다.

public class Person implements Serializable {
	private String name;
	// 나이 추가
    private int age;
    
    // ... 생략 ...
}

여기서 우리는 agenull이 되어도기존에 있던 name 필드엔 데이터가 채워지길 원합니다.
이제 직렬화 해둔 Person 데이터를 역직렬화 해보겠습니다.

java.io.InvalidClassException: ..local class incompatible: stream classdesc serialVersionUID = -12345678909876543321, local class serialVersionUID = 19283746564738291

예외 메세지를 읽어보면 serialVersionUID의 정보가 일치하지 않기 때문에 InvalidClassException 에러가 발생한 것을 알 수 있습니다.
우리는 Person 객체에 serialVersionUID를 설정한 적이 없는데도 말이죠..!

그래서 자바 직렬화 스펙을 확인해보았습니다. 링크

It may be declared in the original class but is not required. 
The value is fixed for all compatible classes. 
If the SUID is not declared for a class, the value defaults to the hash for that class. 
  • SUID(SerialVersionUID) 필수 값이 아니다.
  • 호환 가능한 클래스는 SUID 값이 고정되어 있다.
  • SUID가 선언되어 있지 않으면 클래스의 기본 해시값을 사용한다.

serialVersionUID 를 직접 기술하지 않아도 내부 적으로 serialVersionUID 정보가 추가되며, 내부 값도 자바 직렬화 스펙 그대로 자동으로 생성된 클래스의 해시 값을 이라는 것을 확인할 수 있었습니다. (해시 값은 클래스 구조를 이용해서 생성한다고 합니다.)

그럼 어떤 형태가 좋을까요 ?

public class Person implements Serializable {
	private static final long serialVersionUID = 1L;
    
	private String name;
    private int age;
	
    // . . 생략 . . 
}

"조금이라도 역직렬화 대상 클래스 구조가 변경된다면 에러가 발생해야 한다." 정도의 민감한 시스템이 아니라면 클래스를 변경할 때 직접 serialVersionUID 값을 관리해주는 것이 클래스 변경 시 혼란을 줄일 수 있습니다.

그럼 serialVersionUID만 관리해주면 문제가 없을까요 ?
serialVersionUID 값이 동일할 때에도 문제가 생길 수 있는 부분을 살펴보겠습니다.

  1. 변수명은 같은데 변수 타입이 바뀔 때
public class Person implements Serializable {
	private static final long serialVersionUID = 1L;
    
    private StringBuilder name;
    private int age;
    
    // . . 생략 . . 
}

기존 직렬화된 name 데이터는 String이었지만 StringBuilder 타입으로 변경해봤습니다.

java.lang.ClassCastException: cannot assign instance of java.lang.String to field Person.name of type java.lang.StringBuilder in instance of ~~.Person

혹시 primitive type인 intlong으로 바꾸는건 괜찮지 않을까요?

public class Person implements Serializable {
	private static final long serialVersionUID = 1L;
    
    private String name;
    private long age;
    
    // . . 생략 . . 
}
java.lang.ClassCastException: ~~.incompatible types for field age

역시 ClassCastException 예외가 발생했습니다. 자바 직렬화는 상당히 타입에 엄격하다는 것을 알 수 있습니다.

  1. 직렬화 자바 데이터에 존재하는 멤버 변수가 없애거나 추가했을 때
public class Person implements Serializable {
	private static final long serialVersionUID = 1L;
    
    private String name;
    
    // . . 생략 . . 
}
Person

에러는 발생하지 않았지만 값 자체만 없어졌습니다.
그럼 필드를 추가해보겠습니다.

public class Person implements Serializable {
	private static final long serialVersionUID = 1L;
    
    private String name;
    private int age;
    // 추가된 필드
    private String email;
    
    // . . 생략 . . 
}
Person

이번에도 원하는 형태로 값은 채워졌지만 에러는 발생하지 않았습니다.

정리

  • 특별한 문제가 없으면 자바 직렬화 버전 serialVersionUID의 값은 개발 시 직접 관리해야 합니다.
  • serialVersionUID의 값이 동일하면 멤버 변수 및 메서드 추가는 크게 문제가 없습니다. 그리고 멤버 변수 및 이름 변경은 오류가 발생하지는 않지만 데이터는 누락됩니다.
  • 역직렬화 대상의 클래스의 멤버 변수 타입 변경을 지양해야 합니다. 자바 역직렬화시에 타입에 엄격합니다.
    직렬화된 데이터가 존재하는 상황에 나중에라도 타입 변경이 되면 발생할 예외의 경우의 수를 다 신경 써야합니다.

끝 🔓

Ref.
https://techblog.woowahan.com/2550/
https://techblog.woowahan.com/2551/

profile
⋆。゚★⋆⁺₊⋆ ゚☾ ゚。⋆ ☁︎。₊⋆

0개의 댓글