Java 객체의 직렬화
: 자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 데이터 변환하는 기술
Java에서 입출력은 스트림이라는 데이터 통로를 통해 이동한다. 다만 객체는 바이트형이 아니라, 스트림을 통해 파일에 저장하거나 네트워크에 전송할 수 없다.
따라서 객체를 스트림을 통해 입출력하려면, 바이트 배열로 변환하는 것이 필요하다. 이를 직렬화라고 한다.
시스템 적으로는 JVM(Java Virtual Machine)의 메모리에 상주(heap 또는 stack) 되어 있는 객체 데이터를 바이트 형태로 변환하는 기술과 직렬화된 바이트 형태의 데이터를 객체로 변환해서 JVM으로 상주시키는 형태를 말함.
📌 용어 '직렬화'
사실 직렬화라는 말은 Java에서만 쓰이는 것이 아니며, Computer Science 전반에서 범용적으로 쓰이는 말이다.
자바 직렬화를 하기 위해서 type이 primitive type(기본형 타입)이거나 java.io.Serializable 인터페이스를 상속 받아야 한다.
Serializable
package java.io;
public interface Serializable {}
Serializable 인터페이스를 구현한 User Class 예시
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private String name;
private int age;
private String email;
@Override
public String toString() {
return String.format("User name: %s, age: %s, email: %s", name, age, email);
}
}
위의 클래스 직렬화하기
java.io.ByteArrayOutputStream
, java.io.ObjectOutputStream
을 이용하면 된다.
@Slf4j
public class SerializeTest {
@Test
@DisplayName("Serialize Test")
public void SerializeTest() {
User user = User.builder()
.name("Happykoo")
.age(30)
.email("rudals4549@gmail.com")
.build();
String serializedUserBase64;
try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try(ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(user);
//직렬화(byte array)
byte[] serializedUser = baos.toByteArray();
//byte array를 base64로 변환
serializedUserBase64 = Base64.getEncoder().encodeToString(serializedUser);
log.debug("serializedUserBase64 >> {}", serializedUserBase64);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
```java
serializedUserBase64 >> rO0ABXNyABduZXQuaGFwcHlrb28ubW9kZWwuVXNlcqLhsk0TYPUEAgADSQADYWdlTAAFZW1haWx0ABJMamF2YS9sYW5nL1N0cmluZztMAARuYW1lcQB+AAF4cAAAAB50ABRydWRhbHM0NTQ5QGdtYWlsLmNvbXQACEhhcHB5a29
```
다시 역직렬화하기
java.io.ByteArrayInputStream
, java.io.ObjectInputStream
을 이용하면 된다.
@Slf4j
public class SerializeTest {
...
@Test
@DisplayName("Deserialize Test")
public void DeserializeTest() {
String serializedUserBase64 = "rO0ABXNyABduZXQuaGFwcHlrb28ubW9kZWwuVXNlcqLhsk0TYPUEAgADSQADYWdlTAAFZW1haWx0ABJMamF2YS9sYW5nL1N0cmluZztMAARuYW1lcQB+AAF4cAAAAB50ABRydWRhbHM0NTQ5QGdtYWlsLmNvbXQACEhhcHB5a29v";
byte[] serializedUser = Base64.getDecoder().decode(serializedUserBase64);
try(ByteArrayInputStream bais = new ByteArrayInputStream(serializedUser)) {
try(ObjectInputStream ois = new ObjectInputStream(bais)) {
//역직렬화(byte array -> object)
Object objectUser = ois.readObject();
User user = (User) objectUser;
log.debug(user.toString());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
User name: Happykoo, age: 30, email: rudals4549@gmail.com
transient
키워드를 사용하면, 직렬화하는 대상에서 제외시킬 수 있다.
public class User implements Serializable {
private String name;
private int age;
//직렬화에서 제외
private transient String email;
...
}
객체를 자바 직렬화한 후 데이터(클래스) 구조를 변경하면, 클래스 버전이 맞지 않기 때문에 제대로 역직렬화되지 않음. 에러가 발생함!
serialVersionUID 가 다르다며 에러가 발생한다.
즉, serialVersionUID는 직접 기술하지 않아도 내부적으로 추가되며, 클래스 구조 정보를 이용하여 생성된 해쉬값을 이용한다.
serialVersionUID 를 비교하여 다르다면, 역직렬화가 불가능하다. (클래스 구조가 변경되면 serialVersionUID 도 변경 됨)
클래스 구조가 변경되었을 때, 역직렬화 시 에러를 발생시켜야하는 시스템이 아니라면, serialVersionUID를 개발자가 직접 관리할 수도 있다.
serialVersionUID 설정하기
public class User implements Serializable {
//serialVersionUID 추가
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String email;
...
}
위와 같이 설정해두면, 클래스 버전이 바뀌었어도 제대로 역직렬화된다. (다만 새로 추가된 property의 값은 null로 세팅됨)
역직렬화에 영향을 주는 클래스 구조 변경
따라서 자바 직렬화를 자주 변경되는 클래스의 객체에 사용하는 것을 지양해야 한다.
또한, 또한, 외부(DB, Cache, NoSQL 서버)에 장기간 저장될 정보에 자바 직렬화를 사용하는 것도 피해야 한다. 데이터가 저장되어 있는 동안 데이터 구조가 변경된다면, 그 직렬화된 데이터는 쓰레기(Garbage) 혹은 나중에 에러를 발생시킬 시한폭탄이 되어 버릴 가능성이 높기 때문이다.