점점 강해지고 있습니다.
[Java] 직렬화(Serialization)
들어가기 전에
객체를 저장하거나 메모리, 데이터베이스 혹은 파일로 옮기려면 어떻게 해야할까? 각자 PC의 OS(윈도, 리눅스 등…)마다 서로 다른 가상 메모리 주소 공간을 사용하기 때문에, 원시 타입이 아닌 참조 타입의 데이터들은 인스턴스를 직접 전달 할 수 없다.
이런 문제를 해결하기 위해 플랫폼에 독립적인 Byte 형태로 변환하여 통신하도록 했다.
여기서 객체를 Byte 형태로 변환하는 것, Byte에서 다시 객체로 되돌리는 것을 직렬화(Serialization)라고 통칭한다. HTTP 통신을 할 때는 JSON
, XML
등의 형식을 사용하여 직렬화하여 보내는 경우가 많다.
Java에서의 직렬화란?
Java 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 Java 시스템에서도 사용할 수 있도록 직렬화하는 것
JVM의 메모리에 상주(힙 또는 스택)되어 있는 객체 데이터를 바이트 형태로 변환하는 기술과 직렬화된 바이트 형태의 데이터를 객체로 변환해서 JVM으로 상주시키는 형태를 같이 이야기한다.
Java 직렬화를 위해서는 무조건 Serializable 인터페이스를 상속받아야 한다.
Serializable
이 인터페이스를 보면 구현해야 하는 메서드가 없다. Serializable 인터페이스를 구현한 구현체가 직렬화 대상이다라는 것을 JVM에게 알려주는 역할만 하기 때문이다.
SerialVersionUID
-
클래스 일치여부를 구별하는 기준, 명시적으로 선언하지 않으면 직렬화 시 자동 생성됨
-
역직렬화 과정에서 발신자의 버전과 수신자의 버전이 서로 동일한지를 이 기준을 통해 체크한다. 직렬화된 객체를 역직렬화 할 때엔 직렬화 당시 클래스와 같은 클래스를 사용해야 하는데, 클래스명이 같더라도 내용이 변경된 경우 역직렬화는 실패한다.
SerialVersionUID는 컴파일러 실행에 따라 변경될 수 있는 클래스의 세부 사항에 매우 민감한 반면, 클래스 내에 SerialVersionUID를 정의할 경우 클래스 내용이 변경되어도 클래스 버전은 업데이트되지 않으므로 직렬화 시에는 SerialVersionUID를 정의할 것이 권장된다고 한다.
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Base64;
class Member implements Serializable {
private static final long serialVersionUID = 1234L;
String name;
String email;
int age;
transient String password;
public Member(String name, String email, int age, String password) {
this.name = name;
this.email = email;
this.age = age;
this.password = password;
}
@Override
public String toString() {
return "Member [name=" + name + ", email=" + email + ", age=" + age + ", password = " + password + "]";
}
}
public class Test {
public static void main(String[] args) throws Exception {
Member member = new Member("홍길동", "honggildong@naver.com", 33, "secret");
System.out.println("직렬화 전 데이터 : "+member.toString());
byte[] serializedMember;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(member);
// serializedMember -> 직렬화된 member 객체
serializedMember = baos.toByteArray();
}
}
String base64Member = Base64.getEncoder().encodeToString(serializedMember);
System.out.println("직렬화 후 데이터 : "+base64Member);
byte[] unserializedMember = Base64.getDecoder().decode(base64Member);
try (ByteArrayInputStream bais = new ByteArrayInputStream(unserializedMember)){
try (ObjectInputStream ois = new ObjectInputStream(bais)){
Member outMem = (Member) ois.readObject();
System.out.println("역직렬화된 데이터 : "+outMem.toString());
}
}
}
}
결과
직렬화 전 데이터 : Member [name=홍길동, email=honggildong@naver.com, age=33, password = secret]
직렬화 후 데이터 : rO0ABXNyAAZNZW1iZXIAAAAAAAAE0gIAA0kAA2FnZUwABWVtYWlsdAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgABeHAAAAAhdAAVaG9uZ2dpbGRvbmdAbmF2ZXIuY29tdAAJ7ZmN6ri464+Z
역직렬화된 데이터 : Member [name=홍길동, email=honggildong@naver.com, age=33, password = null]
직렬화의 주된 목적은 객체를 상태 그대로 저장하고 필요할 때 다시 복원하여 사용하는 것이다.
단, 클래스에 비밀번호와 같이 보안상 직렬화되면 안 되는 값이나 직렬화가 될 수 없는 객체에 대한 참조가 포함된 경우 transient라는 제어자를 이용할 수 있다. transient가 붙은 인스턴스 변수의 값은 그 타입의 기본값으로 직렬화되어 위 코드에서도 역직렬화된 데이터의 password는 null이 나오게 됐다.
정리
객체를 byte 형태로 변환하는 것, byte를 객체로 복구하는 것을 직렬화라고 한다.
참고자료
https://jeong-pro.tistory.com/190
https://techblog.woowahan.com/2550/
https://j.mearie.org/post/122845365013/serialization
https://haranglog.tistory.com/4