ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java - 직렬화(Serialization)와 역직렬화(Deserialization)
    Language/Java 2022. 3. 21. 17:48

    데이터 직렬화와 역직렬화

    데이터 직렬화와 역직렬화란?

    데이터 직렬화 : 메모리의 데이터를 디스크에 저장하거나, 네트워크 통신에 사용하기 위한 형식으로 변환하는 것이다.

    데이터 역직렬화 : 디스크에 저장한 데이터를 읽거나 네트워크 통신으로 받은 데이터를 메모리에 쓸 수 있도록 변환하는 것이다.

     

    데이터를 저장하거나 통신하기 위해 직렬화 과정을 거치는 것은 알겠는데, 왜 데이터를 그냥 사용하지 않고 이러한 과정을 거치는 것일까?

     

    직렬화가 필요한 이유

    어떠한 개발 언어를 사용하던지 간에 사용하는 데이터는 다음과 같이 크게 두 가지로 나뉜다.

     

    값 형식 데이터 : int, float, char 등 값 형식 데이터는 Stack에 메모리가 쌓이고 직접 접근이 가능하다.

    참조 형식 데이터 : 객체와 같은 참조 형식 변수를 선언하면 Heap 영역에 메모리가 할당되고, Stack에서는 이러한 Heap 메모리를 참조하는 구조로 되어있다.

     

    위 두 가지 데이터 중에서 디스크에 저장하거나 통신할 때는 값 형식 데이터만 사용할 수 있다. 참조 형식 데이터는 실제 데이터 값이 아닌 Heap에 할당되어 있는 메모리 번지 주소를 가지고 있기 때문이다.

     

    참조 형식 데이터를 사용할 수 없는 이유

    예를 들어, 객체 A를 만들고 주소 값이 0x00045523이라고 가정해 보겠다. 그리고 이 값을 파일에 포함하여 저장했다고 해보자. 이후 프로그램을 종료하고 다시 실행해서 주소 값 0x00045523을 가져오더라도 기존 A 객체의 데이터를 가져올 수 없을 것이다. 왜냐하면, 프로그램이 종료되면 기존에 할당되었던 메모리(0x00045523)는 해제되고 없어지기 때문이다.

    네트워크 통신 또한 마찬가지이다. 각 PC마다 사용하고 있는 메모리 공간 주소는 전혀 다르다. 따라서, 내가 다른 PC로 전송한 객체 A 데이터(0x00045523)는 무의미하다. 이 데이터를 받은 PC의 메모리 주소 0x00045523에는 전혀 다른 값이 존재할 것이다.

     

    직렬화를 하는 이유

    디스크에 저장하거나 네트워크 통신을 할 때 값 형식 데이터만 가능하다는 것은 이해하였다. 그렇다면 직렬화는 왜 필요한 것일까?

    직렬화를 하게 되면 각 주소 값이 가지는 데이터를 전부 끌어 모아서 값 형식 데이터로 변환시켜 준다. 직렬화된 데이터는 언어에 따라서 텍스트 또는 바이너리 등의 형태가 되는데, 이러한 형태가 되었을 때 저장하거나 통신할 때 파싱이 가능한 유의미한 데이터가 된다.

    즉, 직렬화를 하는 이유는 사용하고 있는 데이터를 파일 저장 혹은 데이터 통신에서 파싱할 수 있는 유의미한 데이터를 만들기 위해서이다.

     

    데이터 직렬화의 종류

     

    1. CSV, XML, JSON 직렬화

     - 사람이 읽을 수 있는 형태

     - 저장 공간의 효율성이 떨어지고, 파싱하는 시간이 오래 걸림

     - 데이터의 양이 적을 때 주로 사용

     - 최근에는 JSON 형태를 통해 직렬화를 많이 함

     - 모든 시스템에서 사용이 가능

     

    2. Binary 직렬화

     - 사람이 읽을 수 없는 형태

     - 저장 공간을 효율적으로 사용할 수 있고, 파싱하는 시간이 빠름

     - 데이터의 양이 많을 때 주로 사용

     - 모든 시스템에서 사용이 가능

     - ex) 프로토콜 버퍼, Apache Avro 등

     

    3. Java 직렬화

     - Java 시스템 간의 데이터 교환이 필요할 때 사용


    Java의 직렬화와 역직렬화

    Java의 직렬화와 역직렬화란?

    Java 직렬화

     - Java 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 Java 시스템에서도 사용할 수 있도록 바이트 형태로 데이터를 변환하는 기술

     - JVM의 메모리에 상주(Heap 또는 Stack)되어 있는 객체 데이터를 바이트 형태로 변환하는 기술

     

    Java 역직렬화

     - 바이트로 변환된 데이터를 다시 객체로 변환하는 기술

     - 직렬화된 바이트 형태의 데이터를 객체로 변환해서 JVM으로 상주시키는 기술

     

    직렬화

    직렬화 조건

    public class Person implements Serializable {
        private String name;
        private String email;
        private int age;
    
        public Person(String name, String email, int age) {
            this.name = name;
            this.email = email;
            this.age = age;
        }
    
        @Override
        public String toString() {
            return String.format("Person{'%s', '%s', '%s'}", name, email, age);
        }
    }

     - java.io.Serializable 인터페이스를 구현해야 한다.

     

    직렬화 방법

    public class Main {
        public static void main(String[] args) throws IOException {
            Person person = new Person("홍길동", "gildong@gmail.com", 20);
    
            byte[] serializedPerson;
    
            try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
                    oos.writeObject(person);
    
                    serializedPerson = baos.toByteArray();   // serializedPerson => 직렬화된 person 객체
                }
            }
    
            System.out.println(Base64.getEncoder().encodeToString(serializedPerson));   // 바이트 배열로 생성된 직렬화 데이터를 base64로 변환
        }
    }

     - java.io.ObjectOutputStream을 사용하여 직렬화를 진행한다.

     

    역직렬화

    역직렬화 조건

     - 직렬화 대상이 된 객체의 클래스가 class path에 존재해야 하며 import 되어 있어야 한다. (class path : JVM이 프로그램을 실행할 때, class 파일을 찾는 데 기준이 되는 파일 경로)

     - Java 직렬화 대상 객체는 동일한 serialVersionID를 가지고 있어야 한다.

     

    역직렬화 방법

    public class Main {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            String base64Person = "...생략";
    
            byte[] serializedPerson = Base64.getDecoder().decode(base64Person);
    
            try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedPerson)) {
                try (ObjectInputStream ois = new ObjectInputStream(bais)) {
                    Object objectPerson = ois.readObject();
                    Person person = (Person) objectPerson;
    
                    System.out.println(person);
                }
            }
        }
    }

     - java.io.ObjectInputStream을 사용하여 역직렬화를 진행한다.

     

    Java 직렬화의 장점

     - Java 직렬화는 Java 시스템에서 개발에 최적화되어 있다.

     - 복잡한 데이터 구조의 클래스의 객체라도 직렬화 기본 조건만 지키면 큰 작업 없이 바로 직렬화, 역직렬화가 가능하다.

     - 데이터 타입이 자동으로 맞춰지기 때문에 역직렬화가 되면 기존의 객체처럼 바로 사용이 가능하다.

     

    Java 역직렬화의 단점

    1. 역직렬화 시 클래스 구조 변경 문제

    public class Person implements Serializable {
        private String name;
        private String email;
        private int age;
        
        // 생략
    }

     - 위와 같이 기존 Person 클래스를 직렬화하면 아래와 같은 직렬화된 데이터를 얻을 수 있다.

     

    rO0ABXNyABp3b293YWhhbi5ibG9nLmV4YW0xLk1lbWJlcgAAAAAAAAABAgAESQADYWdlSQAEYWdlMkwABWVtYWlsdAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgABeHAAAAAZAAAAAHQAFmRlbGl2ZXJ5a2ltQGJhZW1pbi5jb210AAnquYDrsLDrr7w=

     - 그런데, 기능이 추가되어 기존 클래스 구조의 변화가 생기는 경우가 있다.

     

    public class Person implements Serializable {
        private String name;
        private String email;
        private int age;
        private String phone;   // phone 속성 추가
        
        // 생략
    }

     - 이 상태에서 직렬화한 데이터를 역직렬화하면 java.io.InvaludClassException이 발생한다.

     - 이것은 각 시스템에서 사용하고 있는 모델의 버전 차이로 인해 생기는 문제이다.

     - 이러한 문제점을 해결하기 위해서는 모델의 버전 간 호환성을 유지하는 SUID(serialVersionUID)를 정의해야 한다.

     

    2. 너무 엄격한 타입 체크

     - 객체의 변수명은 같은데 객체의 변수 타입이 달라지면 타입 예외가 발생한다.

     - 예를 들어, 직렬화한 데이터의 객체의 변수 타입이 String인데, 추후 개발하면서 StringBuilder로 바꿨다면 역직렬화가 불가능하다. 

     - int를 long으로 바꾸는 것 또한 마찬가지이다.

     

    3. 용량 문제

    {"name" : "홍길동", "email" : "gildong@gmail.com", "age" : 20}
    
    serializedPerson (byte size = 146)
    json (byte size = 62)

     - 아주 간단한 객체의 내용도 2배 이상의 용량 차이가 발생한다.

     - 일반적인 메모리 기반의 캐시에서는 데이터를 저장할 수 있는 용량의 한계가 있기 때문에 JSON 형태와 같은 경량화된 형태로 직렬화하는 것이 좋다.

     

    Java 직렬화는 어디서 사용되는가?

    1. 서블릿 세션

     - 세션을 서블릿 메모리 위에서 운용한다면 직렬화를 하지 않지만, 파일로 저장하거나 세션 클러스터링, DB를 저장하는 옵션 등을 선택하게 되면 세션 자체가 직렬화 되어 전달된다.

     - 따라서, 세션에 필요한 객체는 java.io.Serializable 인터페이스를 구현하는 것이 좋다.

     

    2. 캐시

     - Encache, Redis, Memcached 등의 라이브러리 시스템에서 캐시 할 부분을 Java 직렬화된 데이터를 저장해서 사용한다.

     - 다른 직렬화 방식도 사용되지만, Java 직렬화 방식이 가장 간편해서 많이 사용된다.

     

    3. Java RMI

     - 원격 시스템 간의 메시지 교환을 위해서 사용하는 Java에서 지원하는 기술이다.

     - 원격 시스템의 메서드를 호출 시에 전달하는 메시지를 자동으로 직렬화하고, 전달받는 원격 시스템에서는 메시지를 역직렬화하여 사용한다.

     

    Java 직렬화 사용 시 주의사항

     - 외부 저장소로 저장되는 데이터는 짧은 만료 시간이 아니라면 Java 직렬화 사용을 지양한다.

     - 역직렬화 시 반드시 예외가 생길 수 있다는 점을 인지하고 개발한다.

     - 자주 변경되는 비즈니스적인 데이터에 대해 Java 직렬화 사용은 지양한다.

     - 긴 만료 시간을 가지는 데이터는 JSON 등 다른 포맷을 사용하여 저장한다.

     

    serialVersionUID
    public class Person implements Serializable {
        private static final long serialVersionUID = 1L;
    }

     - Java 직렬화 대상 객체는 동일한 serialVersionUID를 가지고 있어야 한다.

     - serialVersionUID를 선언하지 않으며, 내부적으로 클래스의 구조 정보를 이용하여 자동으로 생성된 해시 값이 할당된다.

     - 이러한 이유 때문에, 클래스의 멤버 변수가 추가되거나 삭제되면 serialVersionUID가 달라진다.


    출처

    https://hub1234.tistory.com/26

    https://nesoy.github.io/articles/2018-04/Java-Serialize

    https://techblog.woowahan.com/2550/

    https://techblog.woowahan.com/2551/

    https://machine-geon.tistory.com/166

    https://effectivesquid.tistory.com/entry/%EC%9E%90%EB%B0%94-%ED%81%B4%EB%9E%98%EC%8A%A4%ED%8C%A8%EC%8A%A4classpath%EB%9E%80

    https://steady-coding.tistory.com/576

     

    728x90

    'Language > Java' 카테고리의 다른 글

    Java - Overloading & Overriding  (0) 2022.03.23
    Java - Checked Exception & Unchecked Exception  (0) 2022.03.22
    Java - static  (0) 2022.03.04
    Java - 동일성(identity) & 동등성(equality)  (0) 2022.03.03
    Java - Call by Value & Call By Reference  (0) 2022.02.21

    댓글

Designed by Tistory.