Java

[Java] 직렬화 (Serialization)

DH_0518 2024. 2. 3. 01:11

직렬화(Serialization)란, Java에서 사용되는 Object(객체)나 Data를 다른 환경의 Java에서도 사용할 수 있도록 Byte 형태로 변환하는 기술을 말한다.

 

객체를 잘 생각해 보면 Reference Type이기에, 각각 다른 VMS(Virtual Memory Space)를 가진 OS 환경에서는 주소값이 달라지기 때문에 Reference Type의 인스턴스를 전달할 수 없다. 따라서 주소값이 아닌 Primitive Type의 데이터를 넘긴 후 파싱하여 사용해야 한다. 이때 데이터를 넘기기 전에 사용되는 방법이 Reference Type의 데이터를 Byte 형태의 객체 데이터로 변환하는 '직렬화'이다.

 

 

직렬화(Serialization) & 역직렬화(Deserialization)

 

자바의 직렬화(Serialization) & 역직렬화(Deserialization) 과정

  1. JVM의 힙 / 스택 메모리에 상주하고 있는 Object / Data를 직렬화를 통해 Stream of Bytes(바이트 스트림) 형태로 변환한다
  2. 변환된 데이터를 DB나 File과 같은 외부 저장소에 저장해둔다
  3. 다른 OS 환경(ex. 다른 PC)에서, 외부 저장소에 저장된 Stream of Bytes 형태의 데이터를 가져온다
  4. 가져온 Byte 형태의 데이터에서 serialVersionUID(고유식별번호, SUID)를 확인하여 현재 클래스의 SUID와 동일하다면 역직렬화를 통해 Java 객체로 변환 후 JVM 메모리에 적재한다
  5. 만약 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 인터페이스

// 직렬화를 위해 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

 

☕ 자바 직렬화(Serializable) - 완벽 마스터하기

자바의 직렬화 & 역직렬화 직렬화(serialize)란 자바 언어에서 사용되는 Object 또는 Data를 다른 컴퓨터의 자바 시스템에서도 사용 할수 있도록 바이트 스트림(stream of bytes) 형태로 연속전인(serial) 데

inpa.tistory.com

 

[Java] 직렬화(Serialization) | 👨🏻‍💻 Tech Interview

[Java] 직렬화(Serialization) 각자 PC의 OS마다 서로 다른 가상 메모리 주소 공간을 갖기 때문에, Reference Type의 데이터들은 인스턴스를 전달 할 수 없다. 따라서, 이런 문제를 해결하기 위해선 주소값이

gyoogle.dev

 

[java] 자바 RMI(Remote Method Invocation)

자바 RMI. 원격 함수 호출이다. 클라이언트에서 바로 서버로 접속이 불가능한 환경일때, 가상환경에 인터페이스를 두고 통신하는 방식이다. RMI를 알기 전에 분산 컴퓨팅, 분산 객체를 먼저 알아

drcode-devblog.tistory.com