Search
Duplicate
📒

[Java Study] 12-1. 자바 직렬화

상태
수정중
수업
Java Study
주제
4 more properties
참고

자바의 직렬화 & 역직렬화

NOTE
직렬화란 자바 언어에서 사용되는 객체(Object)를 다른 컴퓨터의 자바 시스템에서도 사용가능하도록 바이트 스트림 형태로 변환하는 기술을 의미한다!
자바 객체 → 바이트 스트림 → DB, file, Kafka …
스트림은 클라이언트나 서버 간에 출발지-목적지 입출력을 위한 데이터 통로를 의미한다.
자바는 스트림의 기본 단위를 바이트(Byte)로 두기 때문에, 네트워크나 데이터베이스로 전송하기 위해 최소 단위인 바이트 스트림으로 변환하여 처리한다.
CSV, JSON을 사용하지 않고 자바 직렬화를 사용하면 자바 시스템에서 간단하게 바로 변환이 가능하기 때문이다. 하지만 직렬화 버전(serialVersionID)값은 직접 관리해야 한다.

직렬화 사용처

NOTE
직렬화를 응용해서 JVM 메모리에서만 상주되던 객체 데이터를 시스템이 종료되더라도 나중에 다시 재사용이 될 수 있을때 영속화할 수 있다.

서블릿 세션

단순히 세션을 서블릿 메모리 위에서 운용한다면 직렬화를 필요로하지 않지만, 단일 세션 데이터를 저장 & 공유가 필요할 때 직렬화를 이용한다.
세션 데이터를 데이터베이스에 저장할 때

캐시

데이터베이스로부터 조회한 객체 데이터를 다른 모듈에서도 필요할 때, 객체를 직렬화해서 파일로 보관하다가 역직렬화해서 사용
요즘은 Redis, Memcached와 같은 캐시 DB를 사용한다.

직렬점

NOTE
JSON과 비교해서 직렬화의 장점에 대해서 간단하게 앞서 소개했지만 실제로는 단점이 굉장히 많은 방법이다.
1.
직렬화는 용량이 크다.
클래스 정보 뿐 아니라, 메타 정보를 가져서 파일 용량이 JSON에 비해 2배이상 차이난다.
2.
역직렬화는 위험하다.
역직렬화 과정에서 공격당할 위험이 있다.
3.
클래스 캡슐화가 깨진다.
직렬화 클래스에 private 멤버가 있어도 외부에 그대로 노출된다.

객체 직렬화 & 역직렬화

NOTE
import java.io.Serializable; // Serializable 구 public class User implements Serializable { private static final long serialVersionUID = 1L; private String name; private transient int age; // transient 키워드는 이 필드가 직렬화되지 않음을 의미합니다. public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public String toString() { return "User{name='" + name + "', age=" + age + "}"; } }
Java
복사
직렬화 & 역직렬화 대상 객체
public class Main { public static void main(String[] args) { User user = new User("John Doe", 30); // 1. 객체를 파일에 직렬화합니다.(user.java -> user.ser) try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) { oos.writeObject(user); // User object serialized: User{name='John Doe', age=30 System.out.println("User object serialized: " + user); } catch (IOException e) { e.printStackTrace(); } // 2. 파일로부터 객체를 역직렬화합니다. (user.ser -> user.java) try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) { User deserializedUser = (User) ois.readObject(); // User object deserialized: User{name='John Doe', age=0} System.out.println("User object deserialized: " + deserializedUser); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } }
Java
복사
직렬화 & 역직렬화 과정
만약 여러개의 객체를 직렬화하고 이를 역직렬화하면 주의해야할 점이 있다.
역직렬화 할 때는 직렬화활 때의 순서와 일치해야한다.
customer 1~3 (직렬화) → customer 1~3 (역직렬화) 순서를 맞춰주어야 한다.
따라서 직렬화할 객체가 많다면, ArrayList와 같이 컬렉션에 저장해서 관리하자.

자바 직렬화 버전 관리

NOTE
Serializable 인터페이스를 구현하는 모든 직렬화된 클래스 serialVersionUID(SUID)라는 고유 식별번호를 부여받는다.
public class User implements Serializable { private static final long serialVersionUID = 1L; // 초기 버전 public String name; public User(String name) { this.name = name; } }
Java
복사
User 버전1 (직렬화 과정 수행)
public class User implements Serializable { private static final long serialVersionUID = 2L; // 변경된 버전 public String name; public int age; // 추가된 필드 public User(String name) { this.name = name; } }
Java
복사
User 버전2
public class Main { public static void main(String[] args) { // 파일로부터 객체를 역직렬화합니다. try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) { User deserializedUser = (User) ois.readObject(); System.out.println("User object deserialized: " + deserializedUser.name); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); if (e instanceof InvalidClassException) { System.out.println("InvalidClassException: serialVersionUID does not match"); } } } }
Java
복사
예외 발생 - 버전1 역직렬화시 예외가 발생하게됨
이 식별 ID는 클래스를 직렬화, 역직렬화 과정에서 동일한 특성을 가지는지 확인하는데 사용되며, 클래스 내부 구성이 수정될 경우 기존 SUID와 다르기 때문에 예외가 발생한다.
단, 직렬화 스펙 상 serialVersionUID값 명시는 필수가 아니고, 따로 명시하지 않는다면 시스템이 런타임에 자동으로 해시함수를 통해 생성하게 된다.

직렬화

객체 직렬화란 자바가 객체를 바이트 스트림으로 인코딩(직렬화)하고 그 바이트 스트림으로부터 다시 객체를 재구성(역직렬화)하는 메커니즘이다.
직렬화된 객체는 다른 VM에 전송하거나 디스크에 저장한 후 나중에 역직렬화할 수 있다.

자바 직렬화의 대안을 찾으라

1997년 자바에 처음으로 직렬화가 도입되었고, 이때부터 보안에 취약하지 않겠냐는 의견이 다수가 이뤘다. 그리고 실제로 지금까지 여러 취약점들이 발견되었고 악용되었다.
직렬화의 근본적인 문제는 공격 범위가 너무 넓고 지속적으로 더 넓어져서 방어하기가 어렵다는 점이다.
ObjectInputStream의 readObject 메서드를 호출하면서 객체 그래프가 역직렬화되기 때문이다.
readObject 메서드는(Serializable 구현했다고 가정) 클래스패스 안의 거의 모든 타입의 객체를 만들어 낼 수 있는 마법과 같은 생성자다.
바이트 스트림을 역직렬화 하는 과정에서 이 메서드는 그 타입들 안의 모든 코드를 수행하고, 이는 즉 모든 코드가 공격범위에 들어간다는 의미이다.
신뢰할 수 없는 스트림을 역직렬화하면 우너격 코드 실행, 서비스 거부 드으이 공격으로 이루어지고, 아무 잘못도 없는 애플리케이션이라도 이런 공격이 취약해질 수 있다.
자바 라이브러리와 널리 쓰이는 서드파티 라이브러리에서 직렬화 가능 타입들을 연구하여 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드들을 찾아보았다.
이러한 메서드들을 가젯이라고 부르며, 여러 가젯을 함께 사용해 가젯 체인을 구성할수도 있다.
가끔식 공격자가 기반 하드웨어의 네이티브 코드를 마음대로 수행할 수 있는 아주 강력한 가젯 체인도 발견되곤 한다.
가젯까지 갈 것도 없이, 역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출될 수 있다.
이런 스트림을 역직렬화 폭탄이라 한다.
직렬화의 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화 하지 않는 것이다.
사실 일반적으로 새로운 시스템에서 직렬화를 써야할 이유는 전혀 없다.
객체와 바이트 시퀀스를 변환해주는 다른 매커니즘들이 많고, 안전하다.
대표적으로 JSON과 프로토콜 버퍼가 존재한다.
JSON은 텍스트 기반이라 사람이 읽을 수 있고, 프로토콜 버퍼는 이진 표현이라 효율이 높다는 점이다.
레거시 시스템 때문에 자바 직렬화를 완전히 배제할 수 없을 때의 차선책은 신뢰할 수 없는 데이터는 절대 역직렬화하지 않는 것이다.

Serializable을 구현할지는 신중히 결정하라

단순 implements에 Serializable만 덧붙여서 직렬화를 구현하면 된다. 이 얼마나 간단한가? 하지만 이는 매우 값비싼 일이다.
Serializable을 구현하면 릴리스한 뒤에는 수정하기가 어렵다. 클래스가 이를 구현하면 직렬화된 바이트 스트림 인코딩도 하나의 공개 API가 된다. 그래서 이 클래스가 널리 퍼진다면 그 직렬화 형태도 영원히 지원해야 한다는것이다.
기본 직렬화 형태에서는 클래스의 private, default 인스턴스 필드마저도 API로 공개하는 꼴이 된다. (캡슐화 깨짐)
Serializable 구현의 2번째 문제는 버그와 보안 구멍이 생길 위험이 높아진다.
Serializable 구현의 3번째 문제는 해당 클래스의 신버전을 릴리스할 때 테스트할 것이 늘어난다.
Serializable 구현 여부는 가볍게 결정할 사안이 아니다.