CS 스터디 질문으로 "Java Serializable로 데이터를 통신할 때 문제점을 설명하시오" 라는 내용으로 자료를 찾아본 후 공부하여 정리한 내용이다.
생각해보니 개발하면서 Serializable과 inputStream, OutputStream을 사용한 적이 그리 많지 않았던 것 같다🤔
(파일 입출력 받을 때 정도?)
자바 직렬화는 언제 사용하면 좋은지, 이에 따른 문제점과 해결방안은 무엇인지 공부해보았다.
해당 글은 https://velog.io/@alsgus92/Java-Serialize에-좀-더-깊이-알아보자 글과 이펙티브 자바, 배달의 민족 기술 블로그를 참고하여 작성하였다. 참조 링크는 맨 하단에 두었다. 레쯔꼬!
Java 직렬화는 Java 시스템간의 data 전달시에만 사용하라
직렬화 구현시에는 시스템 호환성을 고려하여 JSON 형태의 직렬화 방법을 고민해야한다
긴 시간 외부에 저장하는 의미있는 데이터는 자바 직렬화를 사용하지 마라
Java를 기준으로 설명하면 객체를 바이트의 배열로 변환하여 파일, 메모리, 데이터베이스를 통해서 송수신이 가능하도록 하는 것
JVM(Java Virtual Machine 이하 JVM)의 메모리에 있는 객체 데이터를 바이트 형태로 변환하는 기술(직렬화)과 직렬화된 바이트 형태의 데이터를 객체로 변환해서 JVM으로 상주시키는 기술(역직렬화)
직렬화 조건
1. primitive type
2. Serializable 인터페이스 상속한 참조형 객체
ObjectOutputStream (Java 직렬화 사용)
Member member = new Member("김배민", "deliverykim@baemin.com", 25);
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가 현재 project의 class path에 존재하고 import 되어있어야
직렬화와 역직렬화를 진행하는 시스템이 서로 다를 수 있다는 것을 고려해야
자바 역직렬화 대상 객체는 직렬화했던 객체와 동일한 serialVersionUID를 가지고 있어야함
없다면 SHA-1 암호함수를 적용해 자동으로 생성해넣음
private static final long serialVersionUID = 1L;
ObjectInputStream (Java 역직렬화 사용)
// 직렬화 예제에서 생성된 base64 데이터
String base64Member = "...생략";
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);
}
}
클래스 구조 변경 불가
- 클래스 변경 시 직접 serialVersionUID
값을 관리해주어야한다
- 클래스에서 필드 하나만 변경되어도 자동 지정되는 serialVersionUID값이 달라져서 역직렬화가 불가능해지기 때문이다
- 역직렬화시 필드 추가, 타입 변경 등 클래스 구조가 변경되면 java.io.InvalidClassException
발생
- 때문에 장시간 외부에 저장될 정보들은 Java 직렬화 사용을 지양해야
- 역직렬화된 클래스가 언제 변경이 될지 모르고 이 값은 쓰레기 값이 되거나 언제 예외가 터질지 모르는 지뢰가 됨
- 그러다보니 개발자가 직접 컨트롤할 수 없는 객체(프레임워크, 라이브러리)를 직렬화하는 것을 추천하지 않음
직렬화 Data 용량 문제
- 직렬화시에 필요한 클래스의 메타정보도 필요하기 때문에 상대적으로 다른 포맷에 비해 용량이 큼
- 2배에서 최대 10배까지도..!
- 이렇게 직렬화 data로 서버 or DB 통신을 하면 트래픽이 급증 가능
- 스프링에서 기본적으로 지원하는 캐시모듈 (Spring Data Redis, Spring Session, ..) 등이 기본적으로 자바 직렬화 형태로 제공됨
- JSON 같은 다른 형태로 직렬화하여 바꿔주는 것을 고려해야
호환성
- 자바 직렬화를 사용하여 외부 데이터를 저장하면 자바에서만 읽을 수 있음
- JSON으로 저장되어있다면 MySQL 이나 Redis에서도 다른 언어로 읽을 수 있다
버그와 보안 구멍 증가
- 직렬화는 자바의 객체 생성 기본 메커니즘인 생성자를 우회하는 객체 생성 기법
- 생성자가 숨어있음으로 "생성자에서 구축한 불변식을 보장해야하고, 생성 도중 공격자가 객체 내부를 들여다 볼 수 없도록 해야한다"는 사실을 떠올리기 어려워짐
- 신버전 릴리스시 테스트할 부분이 늘어남 (신버전 직렬화하여 구버전으로 역직렬화할 수 있는지 등..)
내부 클래스는 직렬화를 구현하면 안된다
- 내부 클래스는 컴파일러가 자동으로 추가하는 필드들이 있음 (바깥 인스턴스 참조와 유효 범위 안 지역변수 값을 저장하기 위한 필드)
- 때문에 내부 클래스에 대한 기본 직렬화의 형태가 분명하지 않음
- 단, 정적 멤버 클래스는 구현 가능하다
https://velog.io/@alsgus92/Java-Serialize에-좀-더-깊이-알아보자
https://techblog.woowahan.com/2550/
https://techblog.woowahan.com/2551/
이펙티브 자바 아이템 86 Serializable을 구현할 지는 신중히 결정하라