직렬화(Serialization)란, Java에서 사용되는 Object(객체)나 Data를 다른 환경의 Java에서도 사용할 수 있도록 Byte 형태로 변환하는 기술을 말한다.
객체를 잘 생각해 보면 Reference Type이기에, 각각 다른 VMS(Virtual Memory Space)를 가진 OS 환경에서는 주소값이 달라지기 때문에 Reference Type의 인스턴스를 전달할 수 없다. 따라서 주소값이 아닌 Primitive Type의 데이터를 넘긴 후 파싱하여 사용해야 한다. 이때 데이터를 넘기기 전에 사용되는 방법이 Reference Type의 데이터를 Byte 형태의 객체 데이터로 변환하는 '직렬화'이다.
직렬화(Serialization) & 역직렬화(Deserialization)
자바의 직렬화(Serialization) & 역직렬화(Deserialization) 과정
- JVM의 힙 / 스택 메모리에 상주하고 있는 Object / Data를 직렬화를 통해 Stream of Bytes(바이트 스트림) 형태로 변환한다
- 변환된 데이터를 DB나 File과 같은 외부 저장소에 저장해둔다
- 다른 OS 환경(ex. 다른 PC)에서, 외부 저장소에 저장된 Stream of Bytes 형태의 데이터를 가져온다
- 가져온 Byte 형태의 데이터에서 serialVersionUID(고유식별번호, SUID)를 확인하여 현재 클래스의 SUID와 동일하다면 역직렬화를 통해 Java 객체로 변환 후 JVM 메모리에 적재한다
- 만약 SUID가 다르다면 InvalidClassException 예외를 발생시켜, 값 불일치 현상을 방지한다
바이트 스트림(Stream of Bytes) ?
- Stream은 Client <--> Server 간에 출발지 / 목적지로 입출력하기 위한 데이터가 흐르는 통로를 말한다
- Java는 Stream의 기본 단위를 Byte로 두고 있기 때문에, 네트워크나 DB로 전송하기 위해 최소 단위인 바이트 스트림으로 변환하여 처리한다
SerialVersionUID
- 직렬화를 위해 Serializable 인터페이스를 구현하는 모든 클래스는, 버전 관리를 위해 serialVersionUID(SUID)라는 고유식별번호를 부여받는다
- 객체의 직렬화, 역직렬화 과정에서 두 객체(직렬화된, 역직렬화된 객체)가 동일한 특성(클래스의 필드 등)을 가지는지 확인하는 데 사용된다
- SUID를 개발자가 직접 명시해 줄 수도 있지만, 직접 명시해주지 않는다면 시스템이 런타임에 클래스의 이름, 생성자 등과 같이 클래스의 구조를 이용해 자동으로 클래스 안에 생성하게 된다
직렬화가 사용되는 곳
직렬화가 필요한 상황
- 데이터의 영속화
- JVM의 메모리에 상주되어 있는 객체를, 시스템이 종료되더라도 나중에 다시 재사용하기 위해 파일이나 데이터베이스에 저장해야 한다. 데이터를 저장하기 위해 영속화가 진행되고, 영속화 과정에서 직렬화가 사용된다
- 서블릿 세션 (Servlet Session)
- 사용자의 상태를 유지하기 위해 세션을 사용한다. 이 세션에 객체들을 저장하기 위해 직렬화가 사용된다
- 캐시 (Cache)
- 데이터를 메모리 등에 저장하여 빠르게 액세스 하기 위해 캐시에 저장하고, 필요할 때 다시 꺼내온다. 캐시에 데이터를 저장하거나 꺼내오는 과정에서 직렬화 & 역직렬화가 사용된다
- 최근에는 자바 직렬화를 이용하는 것이 아닌 Redis나 Memcached등과 같은 캐시 DB를 많이 사용한다
- 자바 RMI (Remote Method Invocation)
- 원격 시스템 간의 메시지 교환을 위해서 사용하는 자바에서 지원하는 기술로, RMI를 사용하면 객체가 다른 JVM에서 실행 중인 객체의 메서드를 호출할 수 있다
- 최근에는 소켓을 이용하기 때문에 잘 쓰이지 않는다
직렬화 대신 JSON을 사용하면 안 될까?
Q) 외부 파일이나 네트워크를 통해 클라이언트 간에 객체 데이터를 주고받을 때 직렬화가 사용되는데, 그렇다면 JSON 데이터 포맷을 사용하는 게 더 편리하지 않을까?
- A) 직렬화는 오직 자바 프로그램에서만 사용 가능하기에 JSON이 더 범용적이고 활용도가 높은 게 사실이다. 하지만 직렬화는 자바 시스템 개발에 좀 더 최적화되어있고, 자바에서만 지원되는 타입(ex. 컬렉션, 클래스, 인터페이스, etc.)은 JSON으로 변환시켜 사용하기에는 한계가 있다. 그래서 이들을 주고받기 위해서는 각 데이터를 매칭시키는 별도의 파싱을 해주어야만 한다.
그에 반해 직렬화를 이용하면 다른 프로그램에서는 사용하지 못할지라도, 다른 추가적인 작업 없이 쉽게 내보낼 수 있다. 그리고 다른 자바 프로그램에서 역직렬화를 통해 곧바로 다시 이용할 수 있다는 장점이 있기에 직렬화를 사용한다 - 하지만 요즘 추세는 범용적인 JSON을 많이 사용하기에, 목적에 따라 적절히 JSON과 직렬화를 선택해서 사용하면 된다.
직렬화의 단점은 ?
- 직렬화한 클래스에 변경이 일어나면 serialVersionUID가 달라지기 때문에 직렬화해두었던 객체를 역직렬화할 수 없다. 따라서 클래스 변경을 개발자가 예측할 수 없을 때나 자주 변경되는 클래스는 직렬화 사용을 지양해야 한다
- 직렬화 데이터는 '타입, 클래스 메타정보'를 포함하므로 사이즈가 크다. 트래픽에 따라 비용 증가 문제가 발생할 수 있기 때문에 JSON 포맷으로 변경하는 것이 좋다. JSON 포맷이 직렬화 데이터 포맷보다 2~10배 더 효율적이다
직렬화 사용법
직렬화 & 역직렬화에 필요한 객체들
1. Serializable
2. ObjectOutputStream
3. ObjectInputStream
Serializable - 객체 직렬화
- Serializable 인터페이스는 아무런 내용도 없는 마커 인터페이스로, 직렬화를 고려하여 작성한 클래스인지를 판단하는 기준으로 사용된다
- 객체 직렬화를 위해서는 java.io.Serializable 인터페이스를 implements 해야 한다. 그렇지 않으면 NotSerializbleException 런타임 예외가 발생한다
// 직렬화를 위해 Serializable Interface를 Implements한다
public class TestClass implements Serializable {
// 필드
private String name;
private int age;
// 생성자
public TestClass(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "TestClass{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
ObjectOutputStream - 객체 직렬화
- 직렬화(스트림에 객체를 출력)에는 ObjectOutputStream을 사용한다
- 객체가 직렬화될 때, 오직 객체의 인스턴스 필드값 만을 저장한다. static 필드나 메서드는 직렬화하여 저장하지 않는다
/**
* 직렬화 예제
* 1. 바이트 스트림 생성
* 2. 파일 생성
*/
public static void main(String[] args) throws IOException {
// 직렬화할 객체
TestClass testClass = new TestClass("kdh", 28);
// ByteArrayOutputStream을 사용한 직렬화
byte[] serializedTestClass = createByteArray(testClass);
System.out.println("serializedTestClass = " + serializedTestClass);
// FileOutputStream을 사용한 직렬화 -> 파일 생성
createFile(testClass);
}
// 직렬화 - 바이트 스트림 생성
public static byte[] createByteArray(TestClass testClass) throws IOException {
// byte 배열 생성
byte[] serializedTestClass;
// 직렬화
try(ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)
) {
oos.writeObject(testClass);
serializedTestClass = baos.toByteArray();
return serializedTestClass;
}
}
// 직렬화 - 파일 생성
public static void createFile(TestClass testClass) throws IOException{
// 외부 파일명
String fileName = "TestClass.txt";
// 파일 스트림 객체 생성
try (// 직렬화
FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream oos = new ObjectOutputStream(fos)
) {
// 파일 생성
oos.writeObject(testClass);
}
}
ObjectInputStream - 객체 역직렬화
- 스트림을 입력받아 Java에서 다시 객체화하는 역직렬화 과정에는 ObjectInputStream을 사용한다
- 직렬화 대상 객체의 클래스가 외부 클래스인 경우, 클래스 경로(Class Path)에 존재해야 하며, import 된 상태여야 한다
/**
* 역직렬화 예제
* 1. 바이트 스트림 역직렬화
* 2. 파일 역직렬화
*/
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 직렬화할 객체
TestClass testClass = new TestClass("kdh", 28);
// ByteArrayOutputStream을 사용한 직렬화
byte[] serializedTestClass = createByteArray(testClass);
// FileOutputStream을 사용한 직렬화 -> 파일 생성
String fileName = createFile(testClass);
// 직렬화 되었던 객체
System.out.println("직렬화 되었던 객체: "+ testClass);
// 바이트 스트림 역직렬화
TestClass byteDeserialized = byteStreamDeserialize(serializedTestClass);
// 파일 역직렬화
TestClass fileDeserialized = fileDeserialized(fileName);
// 같은지 확인
System.out.println("test1 result: " + byteDeserialized.equals(testClass));
System.out.println("test2 result: " + fileDeserialized.equals(testClass));
}
// 역직렬화 - 바이트 스트림을 객체로
public static TestClass byteStreamDeserialize(byte[] streamOfBytes) throws IOException, ClassNotFoundException {
try (ByteArrayInputStream bais = new ByteArrayInputStream(streamOfBytes);
ObjectInputStream ois = new ObjectInputStream(bais))
{
Object objectTestClass = ois.readObject();
TestClass testClass = (TestClass) objectTestClass;
return testClass;
}
}
// 역직렬화 - 파일을 객체로
public static TestClass fileDeserialized(String fileName) throws IOException, ClassNotFoundException {
try(// 파일 스트림 객체 생성
FileInputStream fileInputStream = new FileInputStream(fileName);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream))
{
// 바이트 스트림을 다시 자바 객체로 변환 (캐스팅 필요)
TestClass testClass = (TestClass) objectInputStream.readObject();
return testClass;
}
}
복습 Question
- 직렬화<->역직렬화 과정은 어떻게 될까?
- 직렬화/역직렬화 과정에서 필요한 객체 세 가지는 무엇일까?
- 직렬화의 장/단점은 무엇일까?
Reference
- 자바의 직렬화 & 역직렬화 : https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%A7%81%EB%A0%AC%ED%99%94Serializable-%EC%99%84%EB%B2%BD-%EB%A7%88%EC%8A%A4%ED%84%B0%ED%95%98%EA%B8%B0
- Java 직렬화(Serialization) : https://gyoogle.dev/blog/computer-language/Java/Serialization.html#java-%E1%84%8C%E1%85%B5%E1%86%A8%E1%84%85%E1%85%A7%E1%86%AF%E1%84%92%E1%85%AA-serialization