직렬화와 역직렬화

SeungHoon·2025년 4월 3일
0

Java

목록 보기
2/5
post-thumbnail

직렬화

  • 자바 언어에서 사용되는 Object 또는 Data를 다른 컴퓨터의 자바 시스템에서도 사용할 수 있도록 바이트 스트림으로 변환하는 과정을 말한다.

역직렬화

  • 바이트로 변환된 데이터를 원래대로 자바 시스템의 Object 또는 Data로 변환하는 기술이다.

바이트 스트림이란?
스트림은 클라이언트나 서버 간의 출발지 목적지로 입출력하기 위한 데이터가 흐르는 통로를 말한다. 자바는 스트림의 기본 단위를 바이트로 하기 때문에, 데이터베이스로 전송하기 위해 최소 단위인 바이트 스트림으로 변환하여 처리한다.

직렬화 vs JSON

  • 자바 직렬화는 외부 파일이나 네트워크를 통해 클라이언트 간에 객체 데이터를 주고 받을 때 사용한다. 그런데 왜 JSON 이 있는데 직렬화를 사용하지?
    • 직렬화는 자바의 고유 기술인 만큼 당연히 자바 시스템에서 개발에 최적화되어 있다.
    • 자바의 광활한 레퍼런스 타입에 대해 제약 없이 외부에 내보낼 수 있다.
      • 예를 들어 자바에만 존재하는 컬렉션, 클래스, 인터페이스는 json으로 보내려면 별도의 파싱 과정이 필요하다. 하지만 직렬화는 그럴 필요가 없음.
  • 하지만 대부분의 경우 시스템 간 통신 표준인 JSON을 사용하는 것을 권장한다. 그 이유는 밑에서 서술.

자바에서 직렬화 하기

  • 직렬화(스트림에 객체를 출력)하고자 하는 클래스에서java.io.Serializable 을 구현하면 된다. (마커 인터페이스)
  • 직렬화 할때는 ObjectOutputStream을 사용한다.
  • 객체가 직렬화될 때 오직 객체의 인스턴스 필드값 만을 저장한다. static 필드나 메서드는 직렬화하지 않는다.
  • 확장자는 뭐든지 상관없지만 .ser, .obj로 직렬화된 데이터임을 명시하는게 좋다.

자바에서 역직렬화 하기

  • 역직렬화(스트림으로부터 객체를 입력) 할 때는 ObjectInputStream 을 사용한다.
  • 이때 직렬화 대상이 외부 클래스라면, 클래스 경로에 존재해야 하고, import 상태여야 한다.
  • 역직렬화를 사용하면 생성자로 객체 초기화 없이 바로 인스턴스화 할 수 있다.
  • 여러개의 객체를 직렬화 / 역직렬화 하는 경우 순서대로 직렬화 / 역직렬화를 해야 한다. 순서를 지키는게 어려우므로 보통은 ArrayList 에 넣어서 이것을 직렬화해버린다. 그럼 순서 생각 안해도 됨.

직렬화 요소 제외

  • 민감한 정보나 직렬화 하고 싶지 않은 정보인 경우 transient을 사용한다.
    • primitive 의 경우 각 타입의 디폴트 값으로 초기화 된다.
    • reference 의 경우 null로 초기화 된다. (고객의 비밀번호) ⇒ 애초에 따로 DTO를 만들어서 비밀번호를 포함시키지 않는 것이 더 좋은 설계이다.
class User implements Serializable {
    private String username;
    private transient String password; // 직렬화 제외

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{username='" + username + "', password='" + password + "'}";
    }
}
  • 아니면 직렬화, 역직렬화에 사용되는 메서드인 readObject(), writeObject() 을 직렬화하고자 하는 클래스 내에서 오버라이딩해서 비밀번호를 제외하고 직렬화하게 할 수도 있다.
	// 커스텀 직렬화: password 제외
    @Override
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject(); // password 제외하고 username만 직렬화
    }

    // 커스텀 역직렬화: password 수동 초기화
    @Override
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        this.password = null; // 복원 안 됨 (직렬화 안 했으므로)
    }

객체 상속 관계에서 직렬화

  • 부모가 직렬화 가능이면 자식은 직렬화 가능이다.
  • 자식이 직렬화 가능이면 부모의 인스턴스 필드는 무시되고 자식 필드만 직렬화 된다.
    • 부모도 하고 싶다면 부모를 직렬화 가능하게 하던지, 직렬화/역직렬화 메서드를 오버라이딩 해야함.
  • 부모에서 기본 생성자가 없다면 InvalidClassException 이 발생할 수 있다. 부모 클래스가 직렬화 불가능하다면 역직렬화하는 과정에서 JVM은 부모 클래스의 인스턴스 필드를 복원하려고 기본 생성자를 호출해 초기화하기 때문이다.

자바 직렬화 버전 관리

  • Serializable 인터페이스를 구현하는 모든 직렬화된 클래스는 serialVersionUID(이하 SUID) 이라는고유 식별번호를 부여 받는다.
  • 직렬화된 객체를 역직렬화했을 때 클래스가 원래와 동일한지 확인하기 위한 id이다.
  • 기본으로는 명시하지 않아도 되지만, 그러면 내부 필드가 추가되면 SUID가 바뀌기 때문에 같은 것을 같지 않다고 말해버린다. 그래서 따로 명시하는게 맞다.
class Member implements Serializable {
    // serialVersionUID 꼭 명시 할 것
    private static final long serialVersionUID = 123L;
    
    private String name;
    private int age;
    private String address;
    
    // private String email; // 새로 추가한 클래스 구성 요소

    ...
}
  • SUID 의 경우 인텔리제이에서 생성해주는 기능을 사용하는게 제일 좋긴하다.
  • SUID을 수동 생성하는 경우에는 대부분의 상황에서 괜찮지만, 멤버 변수의 타입이 바뀔 때는 여전히 문제가 발생한다. (int age -> long age)
  • 따라서 변경될 소지가 있는 클래스의 객체는 그냥 직렬화를 하지 않는 것이 좋다.

Composition 패턴에서의 직렬화

  • 특정 클래스가 직렬화 가능이라고 해도 내부에 인스턴스 객체를 가지고 있다면 (직렬화 안되는 ) 예외가 발생한다.
  • 따라서 인스턴스 객체도 직렬화 가능하게 하던지, transient처리를 해야 한다.

직렬화의 단점

  • 용량이 크다 (JSON에 비해 2배 정도 크다)
  • 역직렬화 과정에서 공격자가 악의적인 바이트코드를 삽입한다면 나는 아무런 보호 없이 공격당하는 거다. (심지어 생성자 없이 인스턴스가 만들어진다)
  • 따라서 가장 좋은 건 역직렬화를 안하는거다. 하지만 불가피하다면 다양한 방어 전략을 사용할 수는 있다.

직렬화할지 안할지는 신중히

  • 릴리즈 후에는 직렬화된 바이트 스트림 인코딩도 하나의 공개 API가 된다. 따라서 그 직렬화 형태를 영원히 지원해야 된다…. 이것도 유지보수에 속해버림
    • 새로운 버전을 릴리즈할 때 구버전의 직렬화 상태가 신버전에서 역직렬화가 되는지 체크해야 한다.
  • 클래스 캡슐화가 깨진다. (역직렬화 하면 다 보임)
  • 버그와 보안에 취약하다.
  • 직렬화 여부는 쉽게 결정하면 안된다.
  • 상속용 클래스와 인터페이스에서는 직렬화 구현에 주의해야 한다.
profile
공유하며 성장하는 Spring 백엔드 취준생입니다

0개의 댓글