직렬화와 역직렬화
Serializable 과 SerialVersionUID 알아보기
정의
역직렬화
외부에서 전송한 데이터를 현재 시스템이 이해할 수 있는 형태로 변환하는 행위
스프링에서는 @RequestBody 와 같은 애너테이션을 통해 웹 사용자가 요청하는 데이터를 수신하여, 시스템 내부의 데이터로 변환할 수 있다.
Serializable
Serializable 이란?
Serializable 은 마커 인터페이스로, JVM 이 해당 객체를 직렬화하거나 역직렬화할 수 있다고 알려주는 인터페이스이다.
마커 인터페이스: 아무 메서드도 선언되어 있지 않는 인터페이스를 의미한다.
내부적으로 Serializable 인터페이스를 구현한 클래스는 ObjectOutputStream을 통해 객체를 직렬화할 수 있다.
Serializable 인터페이스를 구현하지 않은 객체가 직렬화하려고 하면, NotSerializableException이 발생한다.
직렬화시 객체의 특정 상태를 제외하고 싶다면?
transient
특정 필드를 직렬화 과정에서 제외하고 싶을 때 transient 키워드를 사용한다.
직렬화를 위한 과정에서 보안상 민감한 정보나 변환할 수 없는 객체 또는 객체 내부의 속성 값 중 직렬화 과정에서 불필요한 데이터 같은 경우를 제외한다.
자바에서 JSON 을 다룰 때는 transient 같은 키워드를 처리하는 @JsonIgnore 어노테이션으로 불필요한 데이터를 직렬화 과정에서 제외할 수 있다.
자바에서 왜 직렬화를 사용할까?
자바에서 타입은 크게 원시 타입과 참조 타입이 존재한다. 원시 타입은 메모리 주소에 본연의 값이 저장되어 있는 형태이고, 참조 타입은 가상 메모리 공간의 주소를 가르키고 있다.
만약 원시 타입을 외부 클라이언트에게 전송하기 위해 직렬화 과정을 거친다면 이 값 자체로 소통할 수 있게된다.
하지만, 참조 타입을 외부 클라이언트에게 전송한다면 가상 메모리 공간의 참조 주소를 전달하게 되는데 이 전달된 메모리 주소는 전달하고자 하는 "나" 와 전달 받는 "상대방" 의 메모리 공간은 서로 다르다.
나: 1234 -> "A 클래스 의 객체 참조 주소 값"
상대방: 1234 -> "크롬 웹 브라우저"
따라서 이런 불상사를 막기 위해 자바에서는 직렬화를 통해 객체의 상태 데이터를 원시 타입으로 변환한 뒤 외부 클라이언트에게 전달하게 된다.
직렬화를 통해 파일 쓰기, 네트워크 전송 등 외부 클라이언트에게 전송하는 행위를 할 때 유의미한 데이터를 전달할 수 있다.
왜 현대 웹에서는 자바 직렬화 대신 JSON을 쓸까?
자바에서 제공하는 직렬화는 Serializable 이라는 마크 인터페이스로 제공 되지만, 이는 자바라는 언어에 종속적이게 된다.
직렬화를 거쳐 역직렬화를 해야할 때 자바 시스템에서만 사용이 가능하다는 뜻이다.
반면에 JSON 은 역직렬화 대상이 자바가 아닌 파이썬, 고, 자바 스크립트 등 다른 언어임에도 불구하고 범용적으로 직렬화/역직렬화를 제공한다.
현대 웹은 멀티 프로세스 환경으로 돌아가는 시스템에서 만약 자바에만 종속적인 직렬화 기능이 있다면, 과연 사용자들은 이 기능을 사용할까?
이 경우 직렬화를 위해 모든 시스템이 자바로 구성되어 있어야하는데, 이런 제약을 깨트리고 범용적인 편의성을 제공하는 것이 JSON 이기 때문에 현대 웹에서 직렬화 도구로 가장 잘 알려진 것이 JSON이다.
Serializable 인터페이스를 직접 사용하게 되는 경우 문제점
싱글턴 객체를 직렬화 하기 위해
Serializable을 구현하면, 역직렬화 과정에서 사전에 싱글턴 객체를 반환하는 메서드를 호출하지 않고,readObject()를 호출하면서 싱글턴 객체가 아닌 새로운 인스턴스를 반환하여 싱글턴이 깨진다.싱글턴 객체의 문제를 보완하고자
readResolve()메서드를 통해 싱글턴 객체를 반환하도록 정의하면 싱글턴을 보장받을 수 있다. 그 과정에서readObject()에서 생성되는 객체는GC가 자동으로 메모리에서 수거하도록 버려지며,readResolve()를 통해 반환 받은 싱글턴 객체만 사용하게된다.
직렬화 자체는 문제가 아니지만 남이 만든 것을 역직렬화 하는 경우가 위험하다. 바이트 스트림을 역직렬화할 때
ObjectInputStream의readObject()메서드를 호출하게 되는데, 이 때 클래스패스에 존재하는 클래스는 객체로 생성할 수 있으며 객체가 갖고 있는 행위를JVM이 위험 요소를 판단하지 못하고 바이트 스트림을 변환하여 그대로 호출하게 된다.공격자가 깃허브에서 파일을 읽는
readObject()를 구현한 클래스를 보고 악의를 품어 공격을 하려고 한다.
시나리오
공격자가 로컬에서 동일한 클래스의 객체를 만들되, 파일 경로를 담는 필드에
/etc/passwd나 암호화 키가 저장된 위치 등 민감한 경로를 문자열로 삽입하여 직렬화한다.직렬화된 바이트 스트림 데이터를 API 를 통해 요청한다.
서버의
JVM은 페이로드를 정상적인 데이터로 착각하고 역직렬화를 시작한다.readObject가 호출되면서, 공격자가 페이로드에 심어둔 악의적인 파일 경로를 그대로 읽어버려 내부 정보가 유출된다.
결론적으로 핵심은 남이 만든 무언가를 내 프로그램에서 역직렬화 하는 것은 누군가 인터넷에 올린 exe 파일을 다운로드 받아 실행 하는 행위와 마찬가지다
클래스가 변경되어도 역직렬화가 가능할까? with SerialVersionUID
SerailVersionUID 를 정의 하지 않으면 벌어지는 일
자바에서 Serializable 마크 인터페이스를 직접 구현하는 구현체가 SerialVersionUID 를 명시 하지 않는 경우 객체의 상태 값을 기준으로 JVM이 해시 알고리즘을 통해 임의의 값을 생성하여 SerialVersionUID 를 생성한다.
만약 외부에 전송하기 위해 구성한 객체의 상태 정보가 직렬화 후 개발자에 의해 수정 되었다고 가정해보자.

그렇다면, 객체의 상태 값을 기준으로 SerialVersionUID 를 새롭게 생성하게 될테고 이 값은 직렬화 된 객체의 상태 정보와 현재 정보가 다르기 때문에 버전 불일치로 인한 InvalidClassException 예외가 발생한다.
그래서, 클래스의 내부 구성이 달라지더라도 같은 클래스 구성을 직렬화 한다는 것을 명시하는게 SerialVersionUID 이다.
Last updated