Java

직렬화

728x90

그냥 막연하게만 직렬화가 클래스를 바이트 형태로 바꾸는거다 (역직렬화는 그 반대) 정도로만 알고 있었는데 

막상 면접때 말하려니 왜 쓰는지도 모르고 그냥 추상적이게 말하니

허접하게 대답할 수 밖에 없었고 당연히 좋은 결과를 얻을 수도 없었다

그래서 한번 정리를 해보고자 포스팅을 남겼다

 

 

- 가상메모리주소공간

운영체제에서 메모리 용량보다 큰 프로세스에 대해서 처리를 할 때 사용하는게 가상 메모리인데, 서로 다른 운영체제 마다 가상메모리를 다르게 갖기 때문에 Object 같은 타입의 참조값(주소값) 데이터는 다른 운영체제에 전달해봐도 그 의미가 없어진다.

그래서 메모리 참조하는게 아니라 Byte 형태로 변환된 데이터를 전달하게 되면, 가상메모리 문제를 신경쓰지 않고 참조형 데이터에 대한 처리를 할 수 있게 된다.

그래서 직렬화가 필요하고 여태까지 알고 있었던 자바 직렬화 말고도 JSON, CSV, XML 등 여러 데이터 포맷에 대한 직렬화/역직렬화가 가능하다

 

 

- 자바 직렬화가 아닌 다른 데이터 포맷에 대한 직렬화

: 앞서 말한 CSV, JSON 등의 자바가 아닌 다른 데이터 포맷은 자바에서 구현하려면 외부 라이브러리를 차용해야 한다

CSV 는 , 로 데이터를 구분하고 표 형태의 다량 데이터를 다룰 때 사용되는 포맷이며, Apache Common CSV, opencsv 등이 사용된다.

JSON 은 구조화된 데이터로 사용되고 자바에선 Jackson 등을 사용한다

또한 이진 직렬화 방법이라 해서 데이터 변환이나 전송속도에 최적화되어서 직렬화 방법을 제시하는 Protocol Buffer, Apache Avro 등이 있다 이런식의 직렬화 방식은 특정 플랫폼이나 언어에 종속되지 않는다는 장점을 갖는다.

 

 

 

- 자바 직렬화

자바 직렬화는 이름에서 알 수 있듯 자바 환경에 최적화되어서 만들어진 직렬화 방식이다

자바 직렬화에서 갖춰야할 기본조건만 갖추면 손쉽게 직렬화를 할 수 있고 복잡한 데이터 구조를 갖고 있더라도 많은 작업 없이 직렬화/역직렬화를 할 수 있다는 장점이 있다

자바에서 보통 JSON 이나 CSV 등과 같은 다른 데이터 포맷을 통한 직렬화 보다는 자바 직렬화를 쓰는 이유는 자바 직렬화가 갖는 위와 같은 장점때문이다.

 

 

 

- 자바 직렬화를 쓰는 경우

JVM 메모리에만 있던 객체 데이터에 영속성을 부여하고 싶을때 사용해서 네트워크로 전송시에 사용된다

실질적인 사용 예시를 들면 

1) Servlet Session 

Servlet 기반의 WAS (톰캣 등) 는 대부분 세션의 자바 직렬화를 지원하고 이 세션 자체를 파일로 저장하거나 세션 클러스터링을 하거나 DB 에 저장하거나 하는 등 할때 세션 자체를 직렬화해서 사용한다

 

2) Cache

Redis, Memcached 같은 시스템을 사용할때 사용된다

캐시란 그 특성상 자주 쓰이는 것들을 담아둬서 더 빠르게 처리하는 것을 목표로 하는 것 처럼

예를들어 DB 에서 뭔가를 가져와서 처리를 해야할때 동일한 것을 반복적으로 가져오는 경우 DB 에 보낼 요청이 시간이 오래 소요되므로 redis 같은 캐시 서버를 운용하고 이 캐시 시스템에 객체를 담을때 직렬화를 한다

 

 

 

 

- 자바 직렬화의 조건

자바에서 직렬화를 하기 위해선 primitive type 의 경우 이미 그자체로 byte 단위이므로 아무 조건이 없지만

객체 같은 참조형 데이터는 java.io.Serializable 인터페이스를 구현해야한다

(객체는 그 크기가 가변적이고 또 앞서 말한것 처럼 참조형 데이터이므로 다른 OS 에서 가상 메모리 문제 때문에)

하지만 객체의 멤버 중 Transient 가 선언된 케이스는 직렬화 대상에서 제외된다

역직렬화의 경우 직렬화 대상이된 클래스가 ClassPath 에 있어야 한다 

(ClassPath 는 .java 파일을 컴파일한 바이트 코드 .class 파일이 담긴 위치를 의미함)

 

 

 

- 예시 (코드 출처 : https://techblog.woowahan.com/2551/)

- Member.java

import java.io.Serializable;
 
public class Member implements Serializable {
    private static final long serialVersionUID = 1L;
 
    private final String name;
    private final String email;
    private final Integer age;
 
    public Member(String name, String email, Integer age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }
 
    // Getter, toString 생략됨
}
 
cs

 

 

- DeSerializableMain.java

import java.io.*;
import java.util.Base64;
 
public class DeSerializableMain {
    public static void main(String[] args) {
        Member member = new Member("park""test@test.com"20);
 
        byte[] serializedMember = null;
 
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
            try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
                objectOutputStream.writeObject(member);
                serializedMember = byteArrayOutputStream.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
 
        String base64Member = Base64.getEncoder().encodeToString(serializedMember);
 
        System.out.println(base64Member);
 
        byte[] deSerializedMember = Base64.getDecoder().decode(base64Member);
 
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(deSerializedMember)) {
            try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {
                Object objectMember = objectInputStream.readObject();
                Member deMember = (Member) objectMember;
                System.out.println(deMember);
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
 
cs

 

 

- 출력

(직렬화된 Byte 배열 코드를 Base64 인코더로 출력한 String 값과 다시 역직렬화를 통해 얻어온 member 인스턴스의 toString 값)

 

 

 

- serialVersionUID (SUID)

: SUID 값은 사실 위에처럼 직접 선언하지 않아도 클래스 구조 정보를 이용해서 SHA-1 을 이용한 해시값을 기반으로 만들어진다 

그럼에도 직접 선언해서 관리를 해야하는 이유는

자바 클래스의 정보가 바뀌면 serialVersionUID 값도 달라지기 때문이다

같은 클래스라도 serialVersionUID 값이 다르면 에러가 발생하게 된다

또한 멤버 변수의 타입이 바뀌는 경우에도 에러가 나므로 주의해야한다

(멤버 변수를 제거하거나 변수명을 바꾸면 에러는 안나지만 데이터가 누락된다)

 

 

 

- 그렇다면 자바 직렬화는 어떻게 써야 하고 언제 써야 하는가

1) serialVersionUID 값을 직접 관리해라

2) 멤버변수 타입은 바꾸지 말아라

3) 외부 (DB, Cache 등) 에 장기간 저장될 정보는 자바 직렬화 쓰지 말아라

-> 언제 클래스 정보가 바뀔지 모르기 때문

4) 자바 직렬화는 클래스에 대한 메타정보를 가지고 있어서 용량이 JSON 같은 포맷에 비해서 크므로 Redis 같은 메모리 서버에 저장할때 용량 문제에 신경써야 한다 

5) 개발자가 직접 컨트롤하기 힘든 클래스(라이브러리나 프레임워크가 제공하는 것들)은 자바 직렬화 하지 말아라

6) 역직렬화시에 예외가 발생할 가능성이 있으므로 예외처리를 해줘야한다

 

 

 

 

다시 느끼지만 그냥 엉성하게 알고 대충 지나가면

항상 나중에 꼭 피보는거 같다

오늘 편하자고 대충 넘기면 꼭 나중에 다시 돌아오는듯

 

 

 

 

 

- References

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

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

 

 

728x90