1. 개요
직렬화(Serializable)의 개념을 이해하고, Serializable을 구현할 때 주의해야할 점을 알아보자.
2. 직렬화(Serializable)란?
인스턴스 >> Byte
인스턴스를 Byte로 변환하는 것을 말한다. 간혹 클래스를 Byte로 변환하는 것이라고도 하는데 목적 측면에서 보거나, 일반적으로 사용하는 방식을 보면 인스턴스를 직렬화하는게 더 맞는 표현이라고 생각한다.
3. 직렬화를 하는 이유
그렇다면 직렬화를 하는 이유는 뭘까? 도대체 뭘 위해서 직렬화를 하는걸까?
바로 인스턴스를 저장하거나, 전송하기 위함이다.
그럼 인스턴스를 저장하거나 전송하는 이유는 뭘까?
객체의 상태를 저장해야할 상황이 있기 때문이다. 아래 예시를 통해 알아보자.
4. 객체의 상태를 저장해야할 상황
1) HTTP 세션 관리
Spring 웹 어플리케이션에서는 HTTP 세션을 사용해 사용자 상태를 유지한다. 사용자의 ID나 이름, 국적 등과 같은 '상태값'을 세션에 저장해두고 필요할때마다 가져온다.
만약 세션 클러스터링 환경이라면 어떨까? 세션을 다른 서버와 공유해야한다. 정확히 말하면 상태값을 갖고 있는 인스턴스를 다른 서버에 전달해야한다. 이를 위해 직렬화하고, Byte 데이터를 Stream 방식으로 전달하는 것이다.
2) 인메모리 저장소 활용(ex. Redis)
spring Session이나 캐싱기능을 위해 저장소로 Redis를 사용하고자 한다면, Redis에 저장할 인스턴스의 클래스에 Serializable을 구현했을 것이다. 상태 값을 가진 인스턴스를 Redis라는 외부 저장소에 전달해야하기 때문이다. 이때도 마찬가지로 외부로 전달하기 위해 직렬화를 한다.
3) 영속적인 저장 / 데이터 복원
HTTP 세션, 클러스터링 환경, 인메모리 저장소는 서버가 재시작되면 저장된 데이터가 날아간다. 서버가 재시작되도 상태값을 유지하고 싶다면 상태 값을 필드로 구분하여 DB에 저장하는 방법도 있지만, 인스턴스 자체를 DB에 저장하는 방법도 있다. 디스크에 저장하는 방식이다. 이 경우 서버에 문제가 생기거나 재시작이 되어도 데이터를 쉽게 복원할 수 있다.
5. Serializable 주의사항
1) 릴리스 한 뒤에는 수정이 어렵다.
클래스가 Serializable을 구현하면 직렬화된 바이트 스트림 인코딩도 하나의 공개 API가 된다. 누군가 역직렬화를 하면 클래스 구조와 상태값들을 확인할 수 있기 때문이다. 그래서 이 클래스가 널리 퍼지게 된다면, 그 직렬화 형태도 영원히 지원해야 한다. 만약 특정 필드를 제거하거나, 타입을 바꾸는 등의 수정을 한다면, 타 시스템의 역직렬화 과정에서 에러가 발생할 수 있기 때문이다.
직렬화 가능 클래스를 만들고자 한다면, 길게 보고 수정이 일어나지 않는 형태로 주의해서 설계해야한다.
2) 버그와 보안 구멍
객체는 생성자를 사용해 만드는게 기본이다. 역직렬화는 기본 방식을 우회하는 생성 기법이다. 마치 숨은 생성자와 같다. 생성자를 우회한다는건 객체 초기화에 필요한 모든 내부 코드를 무시한다는 측면에서 버그를 유발 가능성을 재기한다.
또한 공격자가 객체 내부를 들여다 볼 수 있다면, 직렬화된 코드를 조작하여 서버로 전달할 수 있다. 해당 서버에서 조작된 메서드를 호출이라도 한다면 공격자가 의도한 작업을 서버 내에서 실행하게 될것이다.
따라서 공격자가 이러한 데이터를 볼 수 없는 환경, 즉 private한 환경에서 사용해야한다.
3) 신버전을 릴리스할 때 테스트가 늘어난다.
직렬화 가능 클래스가 수정되면 신버전 인스턴스를 직렬화 한 후 구버전으로 역직렬화할 수 있는지, 그 반대도 가능한지 테스트 해야한다. 직렬화 가능 클래스의 수가 많을수록 테스트할 횟수도 증가한다.
4) 상속용으로 설계된 클래스나 인터페이스는 Serializable을 구현하거나 확장해선 안된다.
해당 클래스를 클래스를 상속하거나 구현하는 모든 클래스는 앞선 문제들에 위험을 안고가게 된다.
그런데... 직렬화 클래스를 수정하면 역직렬화가 무조건 실패하는게 아닐까? 필자의 경우도 그랬던 기억이 있었다.
6. 직렬화 클래스를 수정하면 역직렬화가 무조건 실패하는거 아냐?
직렬화 클래스를 수정해도 구버전 데이터를 역직렬화할 수 있는 조건이 있다. 아래 두 조건을 모두 만족해야한다.
첫째, SerialVersionUID 가 일치할때
클래스가 변경되었지만 serialVersionUID가 동일하다면 역직렬화가 가능하다. 클래스 구조가 약간 달라져도 serialVersionUID가 같다면 역직렬화를 시도할 수 있다.
둘째, 새로운 필드가 추가되거나 제거됐을 때
- 직렬화된 클래스에 신규 필드를 추가할 경우, 역직렬화 시 해당 필드는 기본값 (null, 0, false) 등으로 설정된다.
- 기존 필드가 삭제되어도 역직렬화가 가능하다. 역직렬화 시 해당 필드는 무시된다.
위 두 조건을 만족하면 직렬화 클래스가 수정되도 역직렬화가 가능하다.
단, 클래스의 인터페이스 혹은 상속 구조를 변경하거나, 필드 타입, 클래스 이름이 변경되면 역직렬화에 실패하게 된다.
serialVersionUID
클래스의 직렬 버전을 나타내는 ID 로 모든 직렬화된 클래스는 이 값을 부여받는다. 만약 필드에 명시한다면 그 값이 ID가 되겠지만, 명시하지 않는다면 클래스의 정보를 기반으로 해시함수를 적용해 UID가 자동으로 부여된다.
즉, 추후 조금의 수정 가능성을 염두해둔다면 SerialVersionUID의 값을 지정해주는 것이 좋다.
7. 정리
Serializable은 구현하기는 아주 쉽지만, 주의해야할 사항이 많다. 때문에 클래스의 여러 버전이 상호작용할 일이 없고, 서버가 신뢰할 수 없는 데이터에 노출될 가능성이 없는 등, 보호된 환경에서만 쓰일 클래스가 아니라면 Serializable 구현은 아주 신중하게 이뤄져야한다. 상속할 수 있는 클래스라면 주의사항이 더욱 많아진다.
그럼 2만
'백엔드 > JAVA' 카테고리의 다른 글
[JAVA] Input Stream, Output Stream, Stream (0) | 2024.01.04 |
---|---|
[Java] JVM, JDK, JRE / 차이 / JVM 구조 (0) | 2023.11.16 |
[Java] 배열 VS 리스트 / 배열 리스트 차이 / 공변 / 비공변 / 실체화 / 소거 (0) | 2023.10.18 |
[JAVA] 마샬링, 언마샬링의 개념 이해하기 (0) | 2023.08.08 |
[Java] @Transactional 너 누구야 / 전파속성 / 동작과정 (2) | 2023.08.02 |