Java 직렬화 및 serialVersionUid
업데이트:
자바 직렬화를 “자바 객체 및 데이터를 다른 외부의 자바 시스템에서 사용하기 위해 byte로 변환하는 기술”로 알고는 있지만 좀 더 상세하게 알고 싶어서 이번 포스트를 작성하게 되었습니다.
자바 직렬화
자바의 직렬화/역직렬화란?
위에서 말했듯이 직렬화는 “자바의 객체 및 데이터를 byte로 변환하는 기술”이고, 역직렬화는 반대로 “byte를 객체나 데이터 형태로 변환하는 기술”입니다. 시스템 적으로는 JVM에 Runtime Data Area(Heap 또는 Stack)에 있는 객체를 byte
로 변환하는 것인데, 좀 더 명확하게 알고 싶어 구글링을 좀 더 해봤습니다.
왜 필요할까?
지금부터 이해를 돕기 위해 Java Object를 기준으로 설명하겠습니다.
Java Object는 JVM > Runtime Data Area > Heap 영역에 존재하게 되는데 이를 참조하기 위해서 주소값을 가지고 참조합니다.
그런데 이 System A의 objA
주소값을 System B로 보낸다면 어떻게 될까요?
당연히 System B에선 해당 주소값에 objA
가 존재하지 않을 것입니다. 그래서 byte
형태로 데이터를 순수 데이터로 변환하여 보내기 위해 직렬화가 필요합니다.
반대로 System B에선 받은 byte
를 objA
로 변환하기 위해 역직렬화가 필요한 것이구요.
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();
}
}
}
}
transient
이때 Address
클래스를 Serializable
구현하도록 하거나 멤버 변수인 address
에 transient
키워드를 붙이면 해결 됩니다.
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
로 선언한 address
는 null
로 출력되는것을 볼 수 있습니다.
serialVersionUID
자바 직렬화에 대해 공부하신 분들이라면 serialVersionUID
에 대해 보신적이 있으실 텐데요.
그냥 간단히 변수명을 봤을땐 “직렬화 버전에 대한 UID”라는 뜻인데, 이게 무슨 말일까요?
Serializable
를 구현하는 경우 직렬화/역직렬화 시 서로의 매핑 정보를 확인하기 위해 serialVersionUID
를 비교하는데 이때 명시적으로 serialVersionUID
가 존재하지 않는다면 컴파일러가 Class
에 대해 해시값을 계산하여 부여합니다.
당연히 클래스 정보가 변경되면 serialVersionUID
가 맞지 않게 되고, InvalidClassException
이 발생하게 됩니다.
InvalidClassException
InvalidClassException
를 확인하기 위한 테스트 코드입니다.
순서는 다음과 같습니다.
- 먼저 변환한
byte
를String
형태로 하기 위해base64
로 인코딩합니다.-
위의
member
객체를byte
로 변환,base64
로 인코딩하면 다음과 같은 문자열이 나옵니다."rO0ABXNyACBjb20uY2l0aXplbi5zZXR0ZXIuZG9tYWluLk1lbWJlctdPnIAc7VdsAgADSgACaWRMAAtkZXNjcmlwdGlvbnQAEkxqYXZhL2xhbmcvU3RyaW5nO0wABG5hbWVxAH4AAXhwAAAAAAAAAAF0ACfsp4HroKztmZQg7YWM7Iqk7Yq47JqpIO2ajOybkOyeheuLiOuLpC50AAbsi5zrr7w="
-
- 클래스 정보를 변경합니다.
-
멤버 변수중
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; } }
-
-
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); } }
InvalidClassException
가 발생하는지 확인합니다.-
위와 같이 역직렬화를 하면
serialVersionUID
가 다르다면서InvalidClassException
가 발생하는 것을 확인할 수 있습니다.
-
명시적인 serialVersionUID
그럼 컴파일러에게 맡기지 말고 명시적으로 클래스에 serialVersionUID
를 추가하면 어떻게 될까요?
Member
클래스에 serialVersionUID
를 1L
로 추가하고 위와 똑같은 테스트를 진행해보겠습니다.
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
제거 전 테스트를 확인해보면
클래스 변경 후
description
제거 후 테스트를 확인해보면
정상적으로 역직렬화가 되는걸 확인할 수 있습니다.
serialVersionUID 제약
하지만 serialVersionUID
을 맞춘다고 하더라도 클래스 변환에 대한 제약이 있습니다.
멤버 변수 타입의 변경
예를 들어 위의 Member
클래스에 name
을 String
에서 StringBuilder
로 변경한다면 ClassCastException
가 발생하게 됩니다.
또, id
를 long
에서 int
로 변경하게 된다면 InvalidClassException
가 발생하게 됩니다.
마무리
지금까지 자바 직렬화/역직렬화에 관한 내용을 알아보았습니다.
결론부터 말하자면 “내가 사용할일이 있을까?”입니다.
계속해서 변경하는 비즈니스에 관련된 클래스에는 적용할 수 없을 뿐더러 다른 곳에 적용하더라도 항상 역직렬화에 대해 Exception
이 발생할 것을 생각하고 개발해야 하기 때문에 가져다주는 큰 장점이 없다면 제가 직접 자바 직렬화/역직렬화를 사용할 일은 없어보입니다.
특히나 요새는 Jackson
이나 Gson
을 사용하여 Json
으로 변경하여 데이터를 주고 받는 경우가 많기 때문에 더욱이 사용할 일이 없지 않을까 싶습니다.
혹시나 더 공부하다가 사용할 곳이 발견된다면 그때 다른 포스트로 다뤄보도록 하겠습니다.
오늘도 제 글을 읽어주셔서 감사합니다.
공부하시는 분들에게 도움이 되었으면 좋겠습니다.😀
댓글남기기