[Java] Serialize에 좀 더 깊이 알아보자

mhyun, Park·2022년 8월 23일
4
post-custom-banner

직렬화란?

Java에서 직렬화(Serialization)란 자바의 객체를 바이트의 배열로 변환하여 파일, 메모리, 데이타베이스 등을 통해서 스트림(송수신)이 가능하도록 하는 것을 의미한다.
즉, Java 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 Java 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 데이터 변환하는 것을 뜻한다.

Java에선 직렬화를 위해 Serializable이라는 Marker interface를 지원하고 있는데
이에 대한 특징과 사용법은 다음과 같다.

 package java.io;
 
 public interface Serializable {
 }
  • 장점
    사용하기 쉽다

  • 단점
    호환성을 고려해야 한다.

Java 직렬화 조건

  • 자바 기본 타입(primitive type)과 java.io.Serializable interface를 상속 받은 객체는 직렬화를 위한 기본 조건을 충족한다.
 public class Member implements Serializable {
    private String name;
    private String email;
    private int age;
    
    public Member(String name, String email, int age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }
    
    // Getter Setter 생략
}

Java 직렬화 사용 방법

java.io.ObjectOutputStream 객체를 이용한다.

Member member = new Member("박아무개", "parkamugae@naver.com", 20);
byte[] serializedMember;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
    try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
        oos.writeObject(member);
        // serializedMember -> 직렬화된 member 객체
        serializedMember = baos.toByteArray();
    }
}

// 바이트 배열로 생성된 직렬화 데이터를 base64로 변환
System.out.println(Base64.getEncoder().encodeToString(serializedMember));

ps)
Base 64란 8비트 2진 데이터를 (플랫폼의) 문자 코드에 영향을 받지 않는 공통 ASCII 영역의 문자들로만 이루어진 일련의 문자열로 바꾸는 인코딩 방식을 가리키는 개념이다.

Java 역직렬화 조건

  • 직렬화 대상이 된 객체의 Class가 현재 project의 Class path에 존재하고 있어야 하며 import 되어 있어야 한다.
  • 직렬화와 역직렬화를 진행하는 시스템이 서로 다를 수 있다는 것을 반드시 고려해야 한다. (같은 시스템이라도 소스 버전이 다를 수 있다)
  • 자바 역직렬화 대상 객체는 직렬화 했던 객체와 동일한 serialVersionUID 를 가지고 있어야 한다. (가지고 있지 않다면, class hash값을 토대로 자동으로 생성해서 이용하고 있음)
private static final long serialVersionUID = 1L;

Java 역직렬화 사용 방법

java.io.ObjectInputStream 객체를 이용한다.

String base64Member = "..." // 직렬화 예제에서 생성된 base64 데이터 
byte[] serializedMember = Base64.getDecoder().decode(base64Member);
try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedMember)) {
    try (ObjectInputStream ois = new ObjectInputStream(bais)) {
        // 역직렬화된 Member 객체를 읽어온다.
        Object objectMember = ois.readObject();
        Member member = (Member) objectMember;
        System.out.println(member);
    }
}

Java 직렬화는 왜! 언제? 쓰일까

Java 직렬화 사용 이유

Java 직렬화 방식 외에도 다른 직렬화 방식들은 CSV, XML, JSON 과 같은 형태가 있다.
이런 직렬화 방식들은 시스템의 고유 특성과 상관없는 대부분의 시스템에서의 데이터 교환 시 많이 사용된다.
하지만 Java 직렬화 형태의 데이터 교환은 Java 시스템 간의 데이터 교환을 위해서 존재한다.

물론, Java 시스템간의 데이터 교환을 위해서 XML, JSON 과 같은 직렬화를 사용해도 되지만
Java Serializable을 사용하는 가장 큰 이유는 개발자의 편의를 위해서이다.

복잡한 데이터 구조를 가진 클래스의 객체라도 직렬화 기본 조건만 지키면 큰 작업 없이 바로 직렬화/역직렬화를 가능할 뿐만 아니라 Data Type이 자동으로 맞춰지기 때문에 관련 부분에 대해 큰 신경을 쓰지 않아도 되기 때문이다.

Java 직렬화 사용처

  • JVM의 메모리에서만 상주되어있는 객체 데이터에 대해 입출력형식에 구애받지 않고 영속화(Persistence)가 필요할 때 사용한다.
  • 시스템이 종료되더라도 없어지지 않는 장점을 가지며 영속화된 데이터이기 때문에 네트워크로 전송도 가능하고 필요할 때 직렬화된 객체 데이터를 가져와서 역직렬화하여 객체를 바로 사용할 수 있다.

1. 캐시 (Cache)

DB를 조회한 데이터가 실시간으로 변경되는 경우가 아니면 메모리나 파일 등의 저장소에 저장한 후 동일한 요청이 오면 저장된 객체를 응답한다. 직렬화를 이용하여 캐시를 이용하면 DB 리소스를 절약할 수 있다.

2. Survlet 기반의 WAS 통신

자체 메모리 위에서만 데이터를 운용한다면 굳이 직렬화하지 않아도 객체를 주고받을 수 있다.
그러나 객체를 DB에 저장하거나 파일로 저장한 객체를 전송하는 등의 상황에선 스트림을 통해 데이터를 보내는데 이때 바이트 단위로 보내야 한다. 반대로 다른 서버로부터 객체를 받아올 때 바이트 단위로 가져와서 다시 객체화하는 역직렬화도 사용된다.

Java 직렬화 문제점 발생 유형

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

앞서 직렬화를 소개할 때 다음과 같은 Member class를 이용했었다.

 public class Member implements Serializable {
    private String name;
    private String email;
    private int age;
    
    // 생성자 생략
    // Getter Setter 생략
}

Base64.getEncoder().encodeToString(serializedMember);
> rO0ABXNyABp3b293YWhhbi5ibG9nLmV4YW0xLk1lbWJlcgAAAAAAAAABAgAESQAD...

하지만, 과제가 진행됨에 따라 전화번호 property 가 추가되게 되었다면..?

 public class Member implements Serializable {
    private String name;
    private String email;
    private int age;
    private String phone; // 추가 됨
    
    // 생성자 생략
    // Getter Setter 생략
}

이 구조에서 기존 직렬화된 Member data를 역직렬화 할 때, 우리가 원하는 동작은
phone 빼고 나머지 값들은 채워지는 것이겠지만.. 아쉽게도 java.io.InvalidClassException 예외가 발생한다.

java.io.InvalidClassException: mhyun2.blog.exam1.Member; local class incompatible: stream classdesc serialVersionUID = -8896802588094338594, local class serialVersionUID = 7127171927132876450

해당 에러를 읽어보면 serialVersionUID 가 일치하지 않다는 것을 알 수 있다.
Java 직렬화를 할 때 SUID 선언이 없다면 내부에서 class의 기본 해시값을 사용하여 유니크한 번호를 생성하여 관리하게 되는데 위의 예제에선 SUID는 따로 관리하지 않아 InvalidClassException 예외가 발생하게 되는 것이다.

즉, Serializable 을 이용시엔 serialVersionUID 를 따로 관리해줘야 되는 것을 알 수 있는데
예를 들어 serialVersionUID = 1L 로 관리할 경우

public class Member implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String name;
    private String email;
    private int age;
    private String phone; // 추가 됨
}

> 역직렬화 시 phone 나머지 변수들엔 값이 채워진 상태로 객체 생성 됨.
public class Member implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String name;
    private String email;
    // private int age;
}

> 역직렬화 시 age 값은 없는채로 객체 생성 됨.

하지만, 다음과 같이 변수들의 Type을 바꾸게 된다면...

public class Member implements Serializable {
    private String name;
    private String email;
   // int -> long 변경 
    private long age;
}

java.io.InvalidClassException: mhyun2.blog.exam1.Member; incompatible types for field age 와 같은 Exception이 발생하게 된다.
즉, Java 직렬화는 타입 변경에 엄격하다는 것을 알 수 있다. 지금과 같이 나중에라도 타입 변경이 되고 직렬화된 데이터가 존재하는 상태라면 발생할 예외를 경우의 수를 다 신경 써서 예외처리를 해줘야한다...

Serializable 객체 변경 시에
직렬화-역직렬화가 버전에따라 정상적으로 이루어지는지 확인을 전부 해줘야한다.

이 때문에 장시간 외부(DB, 캐시 서버, NoSQL 서버 등)에 저장될 정보들은 Java 직렬화 사용을 지양해야한다. 역직렬화 대상의 클래스가 언제 변경이 일어날지 모르는 환경에서 긴 시간 동안 외부에 존재했던 직렬화된 데이터는 쓰레기(Garbage)가 될 가능성이 높으며, 언제 예외가 발생할지 모르는 지뢰 시스템이 될 수도 있기 때문이다.

직렬화 Data 용량 문제

직렬화시에 기본적으로 타입에 대한 정보뿐만 아니라 클래스의 메타 정보도 가지고 있기 때문에 상대적으로 다른 포맷에 비해서 용량이 큰 문제가 있다.
특히 클래스의 구조가 거대해지게 되면 용량 차이가 커지게 되는데, 예를 들어 클래스 안에 클래스 또 리스트 등 이런 형태의 객체를 직렬화 하게 되면 내부에 참조하고 있는 모든 클래스에 대한 메타정보를 가지고 있기 때문에 용량이 비대해지게 된다.

따라서, 해당 직렬화 data를 토대로 서버 혹은 DB 통신을 하게 되면 트래픽이 급증하는 타이밍에 큰 문제를 야기할 수 있으니 JSON 과 같은 다른 형태의 직렬화로 바꿔주는 것을 고려해야 한다.

호환성 문제

지금까지 말했듯이 Java 직렬화는 Java 시스템간의 data 전달시에만 사용된다.
즉, 직렬화 구현시에 추후 시스템의 호환성을 고려하여 JSON 형태의 직렬화 방법을 함께 고민해야하며
상속 또한 class 구조 변경을 높일 수 있기 때문에 Serializable 구현을 지양하는 것이 좋다.

1. What is Serializable? 직렬화란 무엇인가?
2. 자바 직렬화, 그것이 알고싶다. 훑어보기편
3. 자바 직렬화, 그것이 알고싶다. 실무편
4. [Effective Java 규칙74] Serializable 인터페이스를 구현할 때는 신중하라

profile
Android Framework Developer
post-custom-banner

0개의 댓글