[Java] : 직렬화와 역직렬화

dohyoungK·2024년 4월 9일
0

면접 스크립트

목록 보기
22/25

[Java] : 직렬화와 역직렬화


직렬화

직렬화(Serialize)란 자바 언어에서 사용되는 Object나 Data를 다른 컴퓨터의 자바 시스템에서도 사용할 수 있도록 바이트 스트림 형태로 연속적인 데이터로 변환하는 포맷 변환 기술을 말한다.

역직렬화

역직렬화(Deserialize)란 바이트로 변환된 데이터를 원래대로 자바 시스템의 Object나 Data로 변환하는 기술이다.

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


직렬화 vs JSON

직렬화는 외부 파일이나 네트워크를 통해 클라이언트 간 객체 데이터를 주고 받을 때 사용된다.

하지만 JSON이라는 데이터 포맷이 존재하는데 굳이 직렬화가 필요할까?

실제로 JSON은 데이터를 교환할 때 범용적으로 사용되고, 직렬화는 오직 자바 프로그램에서만 사용 가능하지만, JSON 형태 데이터는 파이썬, 자바스크립트 등 다른 시스템에서도 사용 가능하다.

직렬화 장점

  • 직렬화는 자바의 고유 기술이므로 자바 시스템 개발에 최적화 되어 있다.

  • 자바의 수많은 레퍼런스 타입에 대해 제약 없이 외부에 내보낼 수 있다.

기본형 타입이나 배열 같은 타입은 다른 프로그래밍 언어도 공통적으로 사용하는 타입이기 때문에, JSON으로도 충분히 상호 이용 가능하다.

하지만 자바의 컬렉션들이나 클래스, 인터페이스 타입들은 단순 파일 포맷으로는 타입 개수의 한계가 있다. 그래서 이들을 외부로 보내기 위해서는 별도의 파싱 작업이 필요하다.

그에 반해, 직렬화를 이용하면 다른 시스템에서는 사용 못하더라도 별도의 파싱 작업없이 외부로 보낼 수 있다. 그리고 역직렬화를 통해 읽어들이면 데이터 타입이 자동으로 맞춰지기 때문에 클래스 기능을 바로 다시 이용 가능하다.

직렬화 단점

Serializable 인터페이스를 implemets하면 간단히 직렬화가 가능하지만 그에 따른 대가는 매우 비싸다.

  • 직렬화는 객체에 저장된 데이터값 뿐만 아니라 타입 정보, 클래스 메타 정보를 가지고 있으므로 용량을 많이 차지한다. 따라서 DB 등 외부에 저장할 때, 장기간 저장하는 정보는 직렬화를 지양해야 한다.

  • 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메소드를 가젯(gadget)이라 부르는데, 남이 만든 것을 역직렬화하는 과정에서 나도 모르게 공격당할 위험이 있다.

  • 클래스가 Serializable을 구현하게 되면 직렬화된 바이트 스트림 인코딩도 하나의 공개 api가 되므로 그 직렬화 형태도 영원히 지원해야한다. 따라서 Serializable을 구현한 순간부터 객체의 유지보수는 직렬화에 묶이게 된다.


직렬화 & 역직렬화 방법

객체를 직렬화 하기 위해서는 Serializable 인터페이스를 implements 해야한다. 그렇지 않으면 NotSerializableException 예외가 발생한다.

Serializable 인터페이스는 아무런 내용도 없는 마커 인터페이스로, 직렬화를 하겠다는 의도를 표시하는 용도이다.

public class Food implements Serializable {
    String name;
    int price;
    int cal;

    public Food(String name, int price, int cal) {
        this.name = name;
        this.price = price;
        this.cal = cal;
    }

    @Override
    public String toString() {
        return "Food{" +
               "name=" + name +
               ", price=" + price +
               ", cal=" + cal +
               "}";
    }
}
public static void main(String[] args) {
    // 직렬화
    Food food = new Food("pasta", 10000, 500);

    String fileName = "Food.ser";

    try(FileOutputStream fos = new FileOutputStream(fileName); ObjectOutputStream oos = new ObjectOutputStream(fos)) {
        oos.writeObject(food);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Food.ser 파일을 보면 이렇게 직렬화가 된 형태를 볼 수 있다.

public static void main(String[] args) {
	// 역직렬화
    String fileName = "Food.ser";

    try(FileInputStream fis = new FileInputStream(fileName); ObjectInputStream ois = new ObjectInputStream(fis)) {
        Food food = (Food) ois.readObject();
        System.out.println(food);
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}

역직렬화의 결과로 아래와 같이 출력된다.

직렬화 리스트 관리

여러 객체를 직렬화하고 이를 역직렬화한다면 순서가 중요하다. 역직렬화 할 때는 직렬화 할 때의 순서와 일치해야 된다. 따라서 직렬화할 객체가 많다면 ArrayList와 같은 컬렉션에 저장해서 순서를 고려할 필요없이 관리하는 것이 좋다.

public static void main(String[] args) {
	// 리스트 직렬화
    Food pasta = new Food("pasta", 10000, 500);
    Food pizza = new Food("pizza", 15000, 1000);
    Food chicken = new Food("chkicken", 20000, 1000);

    String fileName = "Food.ser";

    List<Food> foods = new ArrayList<>();
    foods.add(pasta);
    foods.add(pizza);
    foods.add(chicken);

    try(FileOutputStream fos = new FileOutputStream(fileName); ObjectOutputStream oos = new ObjectOutputStream(fos)) {
        oos.writeObject(foods);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
public static void main(String[] args) {
	// 리스트 역직렬화
    String fileName = "Food.ser";

    try(FileInputStream fis = new FileInputStream(fileName); ObjectInputStream ois = new ObjectInputStream(fis)) {
        List<Food> foods = (List<Food>) ois.readObject();
        System.out.println(foods);
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}


직렬화 요소 제외

transient 키워드를 사용해 직렬화 대상에서 변수가 제외되도록 할 수 있다. transient가 붙은 인스턴스 변수 값은 그 타입의 기본값으로 직렬화된다.

  • Primitive 타입 : 각 타입의 디폴트 값
  • Reference 타입 : null
public class Food implements Serializable {
    String name;
    int price;
    transient int cal;
}

자바 직렬화 버전 관리

Serializable 인터페이스를 구현하는 모든 직렬화된 클래스는 serialVersionUID 라는 고유 식별번호를 부여받는다. 이 식별 ID는 클래스를 직렬화, 역직렬화하는 과정에서 동일한 특성을 갖는 지 확인하는데 사용된다.

public class Food implements Serializable {
    String name;
    int price;
    int cal;
}

public class Food implements Serializable {
    String name;
    int price;
    int cal;
    
    int sale; // 변경사항
}

위와 같이 클래스에 변경사항이 생겼을 경우 역직렬화시 아래와 같은 예외가 나타나게 된다.

따라서 직렬화 클래스는 serialVersionUID를 직접 명시해 버전을 수동으로 관리하는 것을 권장한다. SUID를 직접 명시하면 클래스의 내용이 변경되어도 버전이 시스템이 자동 생성된 값으로 변경되지 않기 때문이다.

public class Food implements Serializable {
	private static final long serialVersionUID = 123L;
    
    String name;
    int price;
    int cal;
}

이렇게 수동으로 버전 관리를 하더라도 필드 변수의 타입 변경시에는 예외가 나타나기 때문에 그 점을 주의해야 한다.

0개의 댓글