직렬화란?
객체를 바이트 스트림으로 바꾸는 것, 즉 객체에 저장된 데이터를 스트림에 쓰기 위해 연속적인 serial 데이터로 변환하는 것이다.
직렬화의 주된 목적은 객체를 상태 그대로 저장하고 필요할 때 다시 생성하여 사용하는 것이다.
역직렬화(Deserialization)는 직렬화의 반대말로, 네트워크나 영구저장소에서 바이트 스트림을 가져와서 객체가 저장되었던 바로 그 상태로 변환하는 것이다.
Java 직렬화 사용 법
기본 타입(primitive type)과 java.io.Serialiable 인터페이스를 상속받은 객체는 직렬화를 위한 기본 조건을 충족한다.
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.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));
역직렬화를 하기 위해서는 직렬화 대상이 된 객체의 Class가 현재 projec의 Class path에 존재하고 import 되어 있어야 하며, 직렬화했던 객체와 동일한 serialVersionUID를 가지고 있어야 한다. (가지고 있지 않다면, class hash 값을 토대로 자동으로 생성해서 이용하고 있음)
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);
}
}
직렬화의 이유 및 사용처
사용 이유
직렬화 방식 외에도 CSV, XML, JSON과 같은 방식이 존재한다. 이 방식의 직렬화는 시스템의 고유 특성과 상관없는 대부분의 시스템에서의 데이터 교환 시 많이 사용된다. (웹 브라우저 등)
하지만 Java 직렬화 형태의 데이터 교화는 Java 시스템 간의 데이터 교환을 위해서 존재한다.
Java 시스템 간의 교환에서 CSV나 JSON 방식을 사용할 수도 있지만, Java 직렬화를 사용하는 가장 큰 이유는 개발자의 편의를 위해서이다.
복잡한 데이터 구조를 가진 클래스의 객체라도 직렬화 조건만 지키면 큰 작업 없이 바로 직렬화/역직렬화가 가능할 뿐 아니라 Data Type이 자동으로 맞춰지기 때문에 큰 신경을 쓰지 않아도 된다.
사용처
JVM의 메모리에서만 상주되어 있는 객체 데이터에 대해 입출력형식에 구애받지 않고 영속화가 필요할 때 사용한다. 또한 시스템이 종료되더라도 없어지지 않는 장점을 가지며 영속화된 데이터이기 때문에 네트워크로 전송이 가능하고 필요할 때 직렬화된 객체 데이터를 가져와서 역직렬화하여 객체를 바로 사용할 수 있다.
1. 캐시 (Cache)
DB를 조회한 데이터가 실시간으로 변경되는 경우가 아니면 메모리나 파일 등의 저장소에 저장한 후 동일한 요청이 오면 저장된 객체를 응답한다.
직렬화를 이용한 캐시를 사용하게 되면 DB의 리소스를 절약할 수 있다.
2. 서블릿 세션 (Servlet Session)
단순히 세션을 서블릿 메모리 위에서만 운용한다면 직렬화를 필요로 하지 않지만, 객체를 DB에 저장하거나 파일로 저장한 객체를 전송하는 등의 상황에선 스트림을 통해 바이트 단위의 데이터를 보내야 하기 때문에 직렬화가 필요하다.
반대로 다른 서버로부터 객체를 받아올 때 바이트 단위로 직렬화된 객체를 가져와서 다시 객체화하는 역직렬화도 사용된다.
단점 및 유의할 점
단점
직렬화 시에 기본적으로 타입에 대한 정보뿐만 아니라 클래스의 메타 정보도 가지고 있기 때문에 상대적으로 다른 포맷에 비해 용량이 큰 문제가 있다.
예를 들어 클래스 안에 클래스, List, HashMap 등 이런 형태의 객체를 직렬화하게 되면 내부에 참조하고 있는 모든 클래스에 대한 메타정보를 가지고 있기 때문에 용량이 비대해지게 된다.
따라서, 해당 직렬화 데이터를 토대로 서버 혹은 DB 통신을 하게 되면 트래픽이 급증하는 타이밍에 장애가 발생할 수 있으니 JSON과 같은 다른 형태의 직렬화로 바꿔주는 것을 고려해야 한다.
Java 직렬화는 Java 시스템 간의 데이터 전달 시에만 사용되기 때문에 호환성 문제가 있다.
직렬화 구현시에 추후 시스템의 호환성을 고려하여 JSON 형태의 직렬화 방법을 함께 고민해야 하며, 상속 또한 Class 구조 변경을 높일 수 있기 때문에 직렬화 구현을 지양하는 것이 좋다.
유의할 점
특별한 문제가 없으면 자바 직렬화 버전(serialVersionUID) 값은 개발자가 직접 관리해야 한다.
역직렬화는 타입에 엄격하기 때문에 대상 클래스의 멤버 변수 타입 변경을 지양해야 한다.
외부(DB, 캐시 서버 등)에 장기간 저장될 정보는 직렬화 사용을 지양해야 한다. 역직렬화 대상 클래스가 언제 변경될지 모르는 환경에 긴 시간 동안 외부에 존재하게 되면 언제 예외가 발생할지 모르는 상황이 발생할 수 있기 때문이다.
개발자가 직접 컨트롤할 수 없는 클래스의 객체에 대해서는 직렬화를 지양해야 한다.
그런 객체(프레임워크, 라이브러리 등에서 제공하는 클래스의 객체)는 보통 serialVersionUID를 가지고 있어 편의상 직렬화시켜 바로 DB에 저장하는데, 이 부분에서 문제가 발생한다.
"프레임워크, 라이브러리가 업데이트하면서 serialVersionUID를 변경 → 테스트는 문제없지만 운영 시 예외 발생"
이와 관련된 예시는 Spring Security의 SecurityContextImpl 클래스가 있다. serialVersionUID 값이 Spring Security의 버전 값이기 때문에 버전이 변경될 때마다 신경이 쓰인다고 한다.
참고
'멋진 개발자 > Java & Spring' 카테고리의 다른 글
개발자 성장 기록 44 - Concurrent Collection (0) | 2024.04.08 |
---|---|
개발자 성장 기록 43 - Thread Pool (1) | 2024.04.07 |
개발자 취준 기록 39 - Java 버전 별 차이 (0) | 2024.04.04 |
개발자 취준 기록 35 - 추상클래스와 인터페이스의 차이 (0) | 2024.03.30 |
개발자 취준 기록 34 - Hash(Table·Map·Set) (0) | 2024.03.28 |