자바 직렬화가 쓰였던 건 이론적인 이유 말고도, 구체적인 “실무 상황” 이 있다.

옛날 톰캣이나 JBoss 같은 WAS 는 서버를 재시작하면 세션이 날아가거나, 고객이 결제 페이지에 있는데 서버가 재시작되면 정보가 사라져 버렸다.

이러한 문제의 해결 방법으로는 WAS가 세션 객체를 디스크에 직렬화해서 저장하는 것이 필요했다. 그리고 재시작을 하고 난뒤에는 디스크에 저장된 세션을 역직렬화로 다시 복원하여 정보를 유지 했다.

직렬화란 ?

Serializable 이란 자바에서 사용되는 객체 또는 데이터를 자바를 사용하는 또 다른 외부 시스템에서도 사용할 수 있도록 byte 형태로 데이터를 변환하는 기술과 byte 로 변환된 데이터를 다시 객체로 변환하는 기술을 합쳐서 말한다.

시스템 적으로 말해보자면 , JVM 의 메모리에 상주하는 객체 데이터를 byte 형태로 변환하는 기술과 직렬화된 바이트 형태의 데이터를 다시 객체로 변환해서 JVM 으로 상주시키는 형태를 말한다.


한번 깊게 살펴보면,
  • 대부분 OS의 프로세스 구현은 서로 다른 가상메모리주소공간(VAS)를 갖기 때문에 Object 타입의 참조값 데이터 인스턴스를 전달할 수 없다. (전달하더라도, 서로 다른 메모리 공간에서 전달된 참조값이므로 무의미) 때문에 서로 다른 메모리 공간 사이의 데이터를 전달하기 위해서 메모리 공간의 주소값이 아닌 Byte 형태로 직렬화된 객체 데이터를 전달해 사용하는 쪽에서 역직렬화하여 사용할 수 있다.

  • Java 클래스는 객체 안에 객체가 존재할 수 있다. 객체 A안에 들어있는 객체 B를 직렬화하면, 객체 B자체의 데이터를 Primitive type(Byte type)의 데이터로 변환한다. 직렬화된 데이터는 모두 Primitive 타입의 데이터 묶음이며, 파일 저장이나 네트워크 전송 시 파싱할 수 있는 유의미한 데이터가 되는 것이다.

  • JSON, CSV 등의 포맷은 직렬화/역직렬화 시에 특정 라이브러리를 도입하면 쉽게 개발이 가능하며, 구조가 복잡해지면 직접 매핑해줘야한다.

직렬화 조건

  1. Java.io.Serializable 인터페이스를 상속 받은 객체와 Primitive 타입의 데이터
    • 기본자료형(Primitive Type)은 정해진 Byte 변수
  2. 객체의 멤버들 중 Serializable 인터페이스가 구현되지 않은 것이 존재하면 안된다.
  3. transient 가 선언된 멤버는 전송하지 않는다.
    • transient 선언시 직렬화 대상에서 제외됨

왜 사용하는 걸까?

자바 직렬화 형태의 데이터 교환은 자바 시스템 간의 데이터 교환을 위해 존재한다. 자바 시스템끼리 주고 받기 위해서 라고 보면 간단하다.

장점

  • 자바 시스템에서의 개발에 최적화 되어있다. 복잡한 데이터 구조의 클래스 객체일지라도, 직렬화의 기본만 지키면 큰 작업 없이 직렬화/역직렬화가 가능하다.

  • 데이터 타입이 자동으로 맞춰지기 때문에 관련 부분을 크게 신경쓰지 않아도 된다.


언제 어디서 사용 해야 하는 가?

1. JVM 내부에서만 객체 저장 / 복원

  • JVM 의 메모리에서만 상주되어있는 객체를 그대로 영속화(Persistence)가 필요할 때 사용된다.

  • 시스템이 종료되더라도 없어지지 않는 장점을 가지며, 영속화된 데이터이기 때문에 네트워크로 전송이 가능하다.

  • 상황

    • 회사에서 개발한 Java 전용 백엔드 툴이 있고 , 이 툴이 한번에 메모리에 큰 데이터를 로드해서 작업함.
    • 그런데 사용자가 중간에 작업을 저장하고 나갔다가, 다시 실행 할 때 그 상태를 그대로 복원해야함.
    • 다른 언어에서 읽을 필요 없고, 단순히 객체를 파일로 덤프해두면 됨.
  • 언제 발생 ?

    • 오프라인에서 쓰는 Swing/JavaFX앱
    • 데이터 분석 툴 , 이미지 처리 툴 등에서 “저장하기 -> 불러오기” 기능
    • JSON 변환 코드를 짜는 것 보다 ObjectOutputStream 한 번 쓰는게 훨씬 빠르고 간단

2. Servlet Session

  • Servlet 기반 WAS(톰캣, 웹로직 등)들은 대부분 세션의 자바 직렬화를 지원하고 있다. 단순히 세션을 서블릿 메모리 위에서 운용한다면, 직렬화가 필요하지 않지만, 파일로 저장하거나 세션 클러스터링, DB 저장하는 옵션등을 선택하면 세션 자체가 직렬화되어 전달된다. 즉, 세션에 필요한 객체는 java.io.Serializable 인터페이스를 구현하는 것을 추천한다.

  • 상황

    • Spring boot 나 tomcat 기반 웹 서비스에서 서버 재시작이 필요함. ( 배포나 장애 복구 )
    • 세션에 User, Cart 같은 객체가 들어 있는데, 서버가 꺼졌다 켜져도 사용자가 로그아웃 안 되게 유지하고 싶음
    • WAS 는 세션을 디스크에 저장해놨다가 다시 읽어오는데 , 이때 세션에 들어간 모든 객체가 Serializable 이어야함
  • 언제 발생?

    • 세션 유지 기능을 켰을 때 ( 설정)

    • 클러스터링 환경에서 서버 A의 세션을 서버 B로 넘길 때

    • JSON으로 세션을 저장할 수도 있지만, WAS 기본 메커니즘은 Java 직렬화로 저장/복원

3. Cache

  • 자바 시스템에서 퍼포먼스를 위해 캐시(Redis , Ehcache , Memcached) 라이브러리를 많이 사용한다. 캐시할 부분을 자바 직렬화된 데이터를 저장해서 사용된다. 자바 직렬 화만 이용해서만 캐시를 적용하지 않지만, 가장 간편하기 때문에 많이 사용한다.

  • 상황

    • 캐시(Ehcache,redis, memcached 등) 라이브러리를 쓰는데, 캐시 데이터를 디스크에 저장하는 옵션을 켜면 캐시 내부 객체를 직렬화 해야함.
    • json 저장도 가능하지만 , 라이브러리 기본 방식이 java 직렬화인 경우가 많음.
  • 언제 발생 ?

    • 대규모 서비스에서 메모리 절약 위해 캐시를 디스크 스토리지와 병행할 때
    • WAS 재시작 후에도 캐시 내용 유지하려고 할 때

4. Remote Method Inovation

  • RMI는 원격 시스템 간의 메세지 교환을 위해서 사용하는 기술이다. 보통 원격 시스템과 통신을 위해 IP와 포트를 이용해 소켓통신을 하지만, RMI는 그 부분을 추상화해 원격에 있는 시스템의 메서드를 로컬 시스템의 메서드인 것처럼 호출할 수 있다. 원격 시스템의 메서드를 호출할 때 전달하는 메세지를 자동으로 직렬화하여 사용하고, 전달받은 원격 시스템에서 해당 메세지를 역직렬화하여 사용한다.
  • 상황
    • Java 에서 Java 로 원격 메서드 호출을 하는 고전적인 분산 시스템 방식
    • 서버 A의 메서드를 서버 B에서 바로 호출하듯이 쓸 수 있음
    • 이때 메서드 매개변수나 반환값이 객체라면, 네트워크를 통해 전송하기 전에 Java 직렬화가 필요함
  • 언제 발생?
    • 구버전 금융 시스템 , 관공서 프로젝트 등 레거시 환경
    • SOAP 같은 웹서비스가 나오기 전 방식이지만 지금도 유지보수 할 때 만날 수 있음.

정리해보자면,

  • JVM 내부 저장/복원 -> Java 앱끼리만 쓰는 로컬 저장소

  • WAS 세션 복원 -> 서버 재시작 , 세션 클러스터링

  • RMI -> Java 전용 네트워크 호출

  • JCache/Ehcache -> 캐시 영속화

  • 객체가 세션에 저장하지 않는 단순한 데이터 집합이고, 컨트롤러에서 생성되어 뷰에서 소멸하는 데이터의 전달체라면 객체 직렬화는 고려하지 않아도된다.

  • 세션 관리를 스토리지나 네트워크 자원을 이용한다면 객체 직렬화를 해야하며, 메모리에서만 관리한다면 객체 직렬화를 할 필요가 없다.


@Entity
@Getter
@Setter
@toString
public class AllnItem implements Serializable {
    private static final long serialVersionUID = 4140042043347990835L;

    private String chnlId;
    private String salestrNo;
    private String itemId;
    private String siteNo;
    private String applyYn;
}
AllnItem allnItem = new AllnItem("0000000","1234", "1000000001", "1001", "Y");
byte[] selializedAllnItem;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
    try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
        oos.writeObject(allnItem);

        // 직렬화된 AllnItem 객체
        selializedAllnItem = baos.toByteArray();
    }
}

try (ByteArrayInputStream bais = new ByteArrayInputStream(selializedAllnItem)) {
    try (ObjectInputStream ois = new ObjectInputStream(bais)) {

        // 역직렬화된 AllnItem 객체를 읽어온다.
        Object obj = ois.readObject();
        AllnItem allnItem = (AllnItem) obj;
    }
}

SerialVersionUID

Java 직렬화 대상 객체는 동일한 serialVersionUID 를 가지고 있어야 한다. 하지만 , 직접 선언하지 않더라도, 내부적으로 클래스의 구조 정보를 이용해 자동으로 생성된 해쉬 값이 할당되어진다. 그러므로 멤버 변수가 추가되거나 삭제되면 serialVersionUID 값이 달라지게 되는데 , 기존 serialVersionUID 가 변경되면 java.io.InvalidClassException 이 발생하게 된다.

private static final long serialVersionUID = 1L;

즉, 위와 같이 직접 관리를 해야 클래스가 변경되어도 문제없이 직렬화/역직렬화가 가능하다.

이때, serialVersionUID 가 같다고 문제없이 직렬화/역직렬화를 할 수 있는 것은 아니다. 클래스의 멤버 변수 타입이 같아야하며, 멤버 변수를 제거하거나 변수명을 바꾸면 예외는 발생하지 않지만 데이터는 누락된다.


직렬화 사용시 주의사항

  • 특별한 문제가 없으면 serialVersionUID 값은 직접 관리한다.

  • 외부(DB, 캐시서버, NoSQL 서버)에 장기간 저장될 정보는 자바 직렬화사용을 지양해야한다. 역직렬화 대상 클래스가 언제 변경이 일어날지 모르는 환경에서 긴 시간동안 외부에 존재했던 직렬화된 데이터는 Garbage가 될 가능성이 높다.

  • 자주 변경되는 데이터를 자바 직렬화를 사용하지 말자

  • 개발자가 직접 컨트롤 가능한 클래스의 객체가 아닌 경우에는 직렬화를 지양해야한다. 개발자가 직접 컨트롤하기 힘든 객체란 라이브러리, 프레임 워크에서 제공하는 클래스 객체를 말한다.

이러한 객체가 직접 serialVersionUID 를 갖고 있기도해, 개발시 편의상 직렬화 하여 DB 또는 캐시에 바로 저장한다면 그로 인해 많은 문제가 야기된다.

  • 프레임워크/라이브러리 버전업을 하면서 serialVersionUID 변경

즉, 변경에 취약하므로 자바 직렬화 사용시에는 자주 변경되는 클래스의 객체는 사용 안하는 것이 좋다. 역직렬화가 되지 않을 때 예외처리는 기본적으로 필요하다.