티스토리 뷰

백엔드 공부하기/TIL

230822 TIL : 직렬화, 역직렬화

개발중인 감자 2023. 8. 22. 15:58

📌 230822 TIL : 직렬화, 역직렬화


 

직렬화란 ?

- 인스턴스의 상태를 그대로 파일 저장하거나 네트워크으로 전송(serialization)하고, 다시 복원(deserialization)하는 방식이다. 

- 바이트 스트림 형태로 연속적인 데이터로 변환하는 포맷 변환 기술을 사용한다.

- 파일, 네트워크로 전송할 많이 쓰인다.

- 자바에서는 보조스트림을 활용하려 직렬화를 제공한다.

 

📝 바이트 스트림이란? 
스트림은 클라이언트나 서버간의 입출력하기 위한 데이터가 흐르는 통로이다. java는 스트림의 기본 단위가 바이트이므로, 네트워크나 데이터베이스로 전송하기 위해 최소 단위인 바이트 스트림으로 변환하여 처리한다.

 

직렬화의 장점 및 쓰임

객체 데이터를 주고받는 형식으론 JSON 같은 데이터 포맷이 존재한다. 또한 JSON는 웹 뿐만 아니라 파이썬, 자바스크입트에서도 범용적으로 사용이 가능하다. 하지만 왜 직렬화 기능을 사용하는 것일까?

직렬화의 장점 자바의 고유 기술인 만큼 자바 시트템에서 개발에 최적화되어있다. 또한 광활한 레퍼런스 타입에 대해 제약 없이 외부에 내보낼 있다. 자바의 온갖 컬렉션, 클래스, 사용자 커스텀 자료형 타입 직렬화의 기본 조건만 지킨다면 바로 외부로 보낼 있고, 역직렬화를 통해 가져올 있다. 

 

 

직렬화 지켜야할 점

- 역직렬화 할때에는 파일에는 직렬화 순서대로 바이트 문자가 기재되니 직렬화와 순서가 일치해야한다. 즉 person1, person2 으로 직렬화 했다면, 역직렬화 할 때에도 person1, person2 순서로 받아야한다. 따라서 직렬화할 때 ArrayList를 사용하면 관리하기 편하다. 

 

- 상속관계에서 직렬화는, 부모에서 직렬화 인터페이스를 구현했다면 자식은 자동으로 구현이 되지만

자식만 직렬화를 구현했다면, 부모에게서 상속받은 인스턴스 필드는 무시한채 직렬화 된다. 

 

- 직렬화는 인스턴스의 내용이 외부로 유출되는 것이므로 프로그래머가 해당 객체에 대한 직렬화 의도를 표시해야한다. Serializable 인터페이스를 구현하여 해당 의도를 표시할 수 있다. 

 

- 직렬화할 때에는 다음 라이브러리를 사용한다. 

역직렬화: ObjectInputStream(InputStream in): InputStream을 생성자의 매개변수로 받아 ObjectInputStream을 생성
직렬화: ObjectOutputStream(OutputStream out): OutputStream을 생성자의 매개변수로 받아 ObjectOutputStream을 생성

 

- 직렬화한 파일을 역직렬화할 때에는 사용한 인스턴스의 자료형의 구조가 동일해야한다. 예를 들어 name, job 있는 Person 클래스의 인스턴스를 직렬화하고, 그 후에 phone 이라는 구성 요소를 추가했다면 역직렬화할 시에 에러가 뜨게된다. 이럴 경우를 대비하여 serialVersionID(SUID)를 명시하여 클래스의 버전 관리를 해주면 phone 필드를 시스템이 알아서 제외시키고 역직렬화를 진행한다. 

serialVersionID(SUID)?  직렬화된 모든 클래스는 SUID 라는 고유 식별번호를 부여받는다. 직렬화 스펙 상 해당 값 명시는 필수가 아니며, 명시하지 않을 경우 시스템이 자동으로 클래스 안에 생성하게 된다. 하지만 왠만한 상황에서는 직접 명시해주어 관리를 해주는 것이 좋다. 그렇지만 명시하더라도 절대 만능이 아니다. 예를 들어, 필드 타입 (ex int age -> long age) 변경으로 인한 에러같은 경우는 막지 못한다.

 

사용 예시

Serializable 인터페이스 : 구현 코드가 없는 maker interface

- Transient : 직렬화 하지 않으려는 맴버 변수에 사용함 (socket 등 직렬화할 수 없는 객체)

 

Externalizable 인터페이스 : 커스텀 직렬화 방식, 구현 메소드 존재.

- writerExternal() 과 readExternal() 메소드를 구현해야함. 

- 프로그래머가 직접 객체를 읽고 쓰는 코드를 구현할 있음.

 

import java.io.*;

//2) Externalizable 인터페이스 구현한 직렬화 방법.
class Person implements Externalizable{

    String name; //이 필드만 직렬화 하고 싶다면
    String job; 

    public Person() {}

    public Person(String name, String job) {
        this.name = name;
        this.job = job;
    }

    public String toString()
    {
        return name + "," + job;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        //out.writeUTF(job);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        //job = in.readUTF();
    }

}

//1) Serializable 인터페이스 구현한 직렬화방법.
class Person implements Serializable {
    String name;
    transient String job; //직렬화 안됨.
    public Person(String name, String job) {
        this.name = name;
        this.job = job;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", job='" + job + '\'' +
                '}';
    }
}


public class SerializationTest {
    public static void main(String[] args) {
        Person person1 = new Person("이순신", "대표이사");
        Person person2 = new Person("김유신", "사내이사");

        //직렬화
        try (FileOutputStream fos = new FileOutputStream("serial2.txt");
             ObjectOutputStream ois = new ObjectOutputStream(fos)) {

            ois.writeObject(person1);
            ois.writeObject(person2);

        } catch (IOException e) {
            e.printStackTrace();
        }

        //역직렬화
        try (FileInputStream fis = new FileInputStream("serial2.txt");
             ObjectInputStream ois = new ObjectInputStream(fis)) {

            Person pLee = (Person) ois.readObject();
            Person pKim = (Person) ois.readObject();

            System.out.println(pLee.toString());
            System.out.println(pKim.toString());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

 

 

직렬화 문제점 

- 직렬화는 용량이 json보다 2배 이상 차이가 날정도로 크다. 

- 역직렬화를 하게 되면 classpath 안에 모든 타입의 객체를 만들어내기 때문에 보안상 공격당할 수 있다. 따라서 신뢰할 수 없는 데이터는 역직렬화 하면 안된다. 

- 직렬화를 피할 수 없다면 역직렬화 필터링 같은 역직렬화 방어 기법을 사용하는 것도 괜찮다. 하지만 안하는게 제일 좋다.

- 릴리즈 이후 수정이 어렵다.  유지보수가 어려움. 

- 클래스 캡슐화가 깨진다. 직렬화할 클래스에 private 멤버가 있어도, 직렬화를 하게되면 전부 공개되어버리므로 캡슐화가 깨진다.

- 버그와 보안에 취약하다. 

- 상속용 클래스와 인터페이스에 직렬화 구현에 주의해야한다. 

- 내부 클래스는 직렬화를 구현하면 안된다. 그러나 정적 내부 클래스는 구현해도 상관없다.