본문 바로가기
Android/Android 기초

[Android] 직렬화와 Serializable, Parcelable

by 태크민 2024. 12. 17.

직렬화, 역직렬화

  • 직렬화(Serialization) : 객체를 바이트 단위의 연속적 데이터(바이트 스트림)로 변경하는 작업
  • 역직렬화(Deserialization) : 바이트 스트림을 원래 객체로 변환하는 작업


왜 직렬화가 필요한가 ?

데이터를 서버 등으로부터 받아올 때 보통 JSON, XML 등의 형태가 전달되곤 합니다.

이 때, 이를 역직렬화하여 바로 기존 객체처럼 쓸 수 있게하거나, JVM 메모리에만 상주되어있던 그런 객체들을 영속화 (Persistence)시켜 시스템이 종료되더라도 없어지지 않게끔 처리해야할 때(Shared Prefrence / DB 등) 직렬화를 사용합니다.

 


Android Intent 통신 과정에서 직렬화

안드로이드의 경우 Intent를 통해 객체를 주고 받습니다. 

Intent는 프로세스 간 통신을 할 수 있도록 설계되었기 때문에 IPC라는 방법을 사용합니다. Intent는 기본적으로 Parcelable을 구현하고 있습니다.

public class Intent implements Parcelable, Cloneable

 

이것은 Intent 객체 자체가 다른 컴포넌트나 프로세스로 전달될 수 있도록 직렬화 가능함을 의미합니다. 즉, Intent는 직렬화 과정을 거쳐 데이터를 byte 형태로 다른 프로세스에게 전달하는 역할을 수행합니다.

 

하지만 Intent 자체는 직렬화 가능하도록 설계되었지만, Intent에 객체를 담는다면 어떨까요?

Intent 자체는 직렬화가 가능하지만, Intent에 담아서 전달하려는 객체가 있다면 이야기가 달라집니다.

val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("user", user)

 

위 코드에서 user는 단순한 기본 타입이 아닌 "객체"입니다. 이러한 객체는 시스템이 내부적으로 직렬화 방법을 알지 못하기 때문에, 개발자가 직접 직렬화 가능하도록 처리해줘야합니다.

이 때 사용할 수 있는 방법은 두 가지입니다.

  1. Parcelable 인터페이스 구현 (Android 전용, 성능 우수)
  2. Serializable 인터페이스 구현 (Java 표준, 다소 느림)

그렇다면 기본 데이터 타입은 왜 직렬화를 안할까요?

Int, String, Boolean과 같은 기본 데이터 타입은 Android 프레임워크 내부에서 이미 바이트 단위로 처리 방식이 정의되어 있기 때문에, 별도로 직렬화 과정을 거칠 필요가 없습니다.

하지만, 객체는 바이트 단위로 처리가 되지 않으므로 직접 직렬화를 해주어야 합니다.

 


Serializable 이란 무엇인가?

Serializable 은 Android SDK 가 아닌 표준 Java 의 인터페이스 입니다.

이 인터페이스를 구현한 클래스의 객체는 이제 한 액티비티 에서 다른 액티비티 로 이동할 준비가 됩니다. 다음 코드에서 이 인터페이스를 사용하는 것이 얼마나 간단한 지 알 수 있습니다.

 

특이한 점은, 이 인터페이스에는 별도로 구현해야 하는 메서드가 없습니다. 이러한 인터페이스를 마커 인터페이스 (Marker Interface)라고 부릅니다.

package java.io;

public interface Serializable {
}

 

Serializable은 해당클래스가 직렬화 대상이라고 알려주기만 할뿐 어떠한 메서드도 가지지 않는 단순한 “마커 인터페이스 (Marker Interface)” 이므로, 사용자는 매우 쉽게 사용할 수 있습니다.

 

하지만, 사용방법이 쉽다는 것은 곧 시스템 적인 비용이 비싸다는것을 의미 합니다.

Serializable은 런타임에 JVM이 리플렉션으로 클래스의 필드 정보를 읽어 직렬화합니다.

즉, 필드 구조, 타입 등을 실행 도중에 분석해서 직렬화/역직렬화가 이뤄집니다.

 

Reflection은 프로세스 동작 중에 사용되며 처리 과정 중에 많은 추가 객체를 생성 합니다. 이 많은 쓰레기들은 가비지 컬렉터의 타겟이 되고 가비지 컬렉터의 과도한 동작으로 인하여 속도성능 저하배터리 소모가 발생합니다.

리플렉션(Reflection)이란?
객체를 통해 클래스의 정보를 분석해 내는 프로그래밍 기법을 말합니다. 
리플렉션은 구체적인 클래스 타입을 알지 못해도 컴파일된 바이트 코드를 통해 그 클래스의 메소드, 타입, 변수 들을 접근할 수 있도록 해주는 자바 API입니다.
(프로그램 실행 중에 객체를 동적으로 조작할 수 있는 기능을합니다.)

리플렉션은 속도를 저하시키기에 구글에서 권장하지 않습니다.

 

직렬화 과정

직렬화는 아래 예시처럼 사용할 수 있습니다.

public static void main(String[] args) {
  Person person = new Person("JaesungLeee", 30);

  byte[] byteStream = serialize(person);
}

public static byte[] serialize(Object o) throws IOException {
  try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
    try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
      oos.writeObject(o);
      return baos.toByteArray();
    }
  }
}

 

먼저, Person이라는 객체는 코드상으로는 남겨두지 않았지만 Serializable 인터페이스를 구현한 직렬화 가능한 클래스입니다. 이 클래스를 직렬화 하기 위해 ObjectOutputStream#writeObject를 통해 직렬화를 수행합니다.

public final void writeObject(Object obj) throws IOException {
    ...
    try {
        writeObject0(obj, false);
    } catch (IOException ex) {
      ...
      throw ex;
    }
}

private void writeObject0(Object obj, boolean unshared) throws IOException {
    ...
    if (obj instanceof String) {
      writeString((String) obj, unshared);
    } else if (cl.isArray()) {
      writeArray(obj, desc, unshared);
    } else if (obj instanceof Enum) {
      writeEnum((Enum<?>) obj, desc, unshared);
    } else if (obj instanceof Serializable) {
      writeOrdinaryObject(obj, desc, unshared);
    } else {
      if (extendedDebugInfo) {
        throw new NotSerializableException(cl.getName() + "\n" + debugInfoStack.toString());
      } else {
        throw new NotSerializableException(cl.getName());
      }
    }
    ...
}

 

writeObject는 내부적으로 ObjectOutputStream#writeObject0을 실행하고, String, Array, Enum, Serializable 이외 타입에 대해서는 예외를 던지는 것을 확인할 수 있습니다.

 

역직렬화 과정

역직렬화도 아래 예시처럼 사용할 수 있습니다.

public static void main(String[] args) {
  Person person = new Person("JaesungLeee", 30);
  
  byte[] byteStream = serialize(person);
  
  Person newPerson = (Person) deserialize(byteStream);
}

public static byte[] serialize(Object o) throws IOException {
  ...
}

public static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
  try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes)) {
    try (ObjectInputStream ois = new ObjectInputStream(bais)) {
      return ois.readObject();
    }
  }
}

 

직렬화된 byte-stream은 ObjectInputStream#readObject를 통해 객체를 얻은 후, 타입 캐스팅을 통해 원본 객체로 역직렬화하게 됩니다. 여기서 중요한 점은 해당 객체는 반드시 동일한 serialVersionUID를 가져야 합니다.

 

serialVersionUID (SUID)

Serializable 인터페이스를 구현하는 모든 객체는 고유의 식별번호 (SUID)를 부여 받습니다. 이 식별번호를 통해 직렬화, 역직렬화 시 동일한 객체인지를 확인하게 됩니다. 따라서, 객체를 구성하는 멤버들의 구성이 수정될 경우 이 식별번호가 달라지기 때문에 예외 (InvalidClassException)가 발생되게 됩니다.

그렇다고 serialVersionUID를 반드시 명시해야 하는 것은 아닙니다. 객체에 식별번호를 명시하지 않더라도 런타임에 JVM에서 별도의 해시함수를 통해 자동으로 식별번호를 생성하게 됩니다. 하지만, 안전한 직렬화, 역직렬화를 위해서는 명시하는 것이 좋습니다.

가령, 요구사항이 변경되어 객체에 새로운 필드를 추가해야 할 경우에는 식별번호가 일치하지 않아 예외가 발생될 수 있습니다. 이 경우 수동으로 식별번호를 명시할 수 있으며, 왠만하면 직접 명시하여 버전을 수동으로 관리하는 것을 권장하고 있습니다.

 

안드로이드에서의 Serializable 처리

위의 코드에서 직렬화 시 serialize(object), 역직렬화 시 deserialize(byteStream) 코드를 호출하는 것을 볼 수 있었습니다.

하지만, Android에서는 Intent를 통해 객체를 저장하고 복원할 때 명시적으로 serialize/deserialize 코드를 호출하지 않아도 됩니다. 이는 안드로이드 시스템에서 내부적으로 직렬화/역직렬화를 자동으로 해주기 때문입니다.

물론, 파일에 객체를 저장하거나 복원할 때는 직렬화와 관련된 코드를 호출해야할 때가 있지만, Serializable/Parcelable을 사용하지 않고 Json/Gson등을 활용하여 객체를 저장할 수도 있습니다.

 


Parcelable 은 무엇인가?

Parcelable 은 직렬화를 위한 또다른 인터페이스 입니다. Serializable 과는 달리 표준 Java 가 아닌 Android SDK 의 인터페이스 입니다.

package android.os;

public interface Parcelable {
  ...
  
  public @ContentsFlags int describeContents();
  
  public void writeToParcel(@NonNull Parcel dest, @WriteFlags int flags);
  
  public interface Creator<T> {
    public T createFromParcel(Parcel source);
      
    public T[] newArray(int size);
  }
  
  ...
}

 

Parcelable은 직렬화 처리 방법을 사용자가 명시적으로 작성해야 합니다. Serializable과 달리 자동으로 처리되지 않으며, 이에 따른 Reflection도 존재하지 않습니다.

따라서, Parcelable은 Reflection을 사용하지 않아 런타임 시 객체 생성할 필요가 없어 GC의 부담이 없어지고, 속도 성능이 좋습니다.

 

하지만, Parcelable 을 사용할 때 지불해야하는 비용도 있습니다.

마커 인터페이스 인 Serializable 과는 달리 Parcelable 은 구현 해야 하는 필수 메서드를 포함 하기 때문에 클래스에 보일러 플레이트 코드가 추가 됩니다. 이는 클래스 를 이해 하기 어렵고, 새로운 기능을 추가 하기 힘들게 만듭니다. 또한 코드의 추가로 클래스가 복잡해 질수록 유지 보수가 어려워지는 원인이 됩니다.

Serializable이 시스템 비용이 발생한다면 Parcelable 은 구현 과 유지, 보수에 드는 사용자의 노력을 비용으로 지불해야 합니다. 

 

직렬화 과정

직렬화는 Parcelable#writeToParcel을 통해 이뤄집니다. Serializable은 ObjectInputStream을 통해 직렬화 할 데이터를 작성했다면, Parcelable은 Parcel이라는 객체를 통해 직렬화 할 데이터를 작성합니다.

class Person() : Parcelable {
  var name: String? = null
  var age: Int? = null
  
  ...
  
  override fun writeToParcel(dest: Parcel?, flags: Int) {
    dest?.run {
      writeString(this@Person.name)
      writeInt(this@Person.name)
    }
  }
  
  ...
}

 

역직렬화 과정

역직렬화는 Parcelable의 하위 인터페이스인 Creator#createFromParcel을 통해 수행할 수 있습니다.

class Person() : Parcelable {
  var name: String? = null
  var age: Int? = null
  
  ...
  
  override fun writeToParcel(dest: Parcel?, flags: Int) {
    dest?.run {
      writeString(this@Person.name)
      writeInt(this@Person.name)
    }
  }
  
  companion object CREATOR : Parcelable.Creator<Person> {
    override fun createFromParcel(parcel: Parcel): Person {
      return Person(parcel)
    }

    override fun newArray(size: Int): Array<Person?> {
      return arrayOfNulls(size)
    }
  }
}
 

@Parcelize Annotation

Android 의존성을 갖는 Parcelable을 사용하여 직렬화를 개발자가 직접 구현할 수 있다는 장점이 있지만, 많은 보일러-플레이트를 만들게 된다는 단점도 존재합니다. 이러한 단점은 가독성을 해치기도 할 뿐만 아니라 새로운 기능의 확장이 어려워집니다. 이를 위해, 안드로이드에서는 @Parcelize Annotation을 제공하고 있습니다.

kotlin-parcelize 플러그인은 Parcelable의 구현을 자동으로 해줍니다. 즉, Annotation의 추가 만으로도 직접 Parcelable 관련 코드를 오버라이드하여 작성하지 않아도 동일하게 동작합니다. 조금 더 복잡한 직렬화 로직이 필요할 경우 선택적으로 구현할 수 있습니다.

 

Parcelize 는 아래와 같이 build.gradle 에 플러그인을 추가하기만 하면 사용할 수 있습니다.

plugins {
    id("kotlin-parcelize")
}

 

사용법은 Serializable 과 같이 매우 간단합니다. 클래스에 @Parcelize 어노테이션을 달면, Parcelable 구현이 자동으로 생성되는 것입니다.

import kotlinx.parcelize.Parcelize

@Parcelize
class User(val name: String, val email: String): Parcelable

 

이를 통해 Parcelable의 단점이였던 보일러 플레이트 코드 문제를 해결하고, 구현하기 쉽고 속도 성능까지 챙길 수 있게 됩니다.

※ 단, Parelize를 사용하면 직렬화된 모든 속성이 기본 생성자(Prmiary constructor)에 선언되어 있어야하며, abstract나 sealed 클래스를 허용하지 않는다는 점을 유의해야 합니다.

 


어느 것을 사용해야하는가?

많은 곳에서 Serializable 이 성능적으로 저하를 유발할 수 있기에 Parcelable 을 권장하는 것을 볼 수 있습니다. Kotlin 의 Parcelize 지원으로 이제 보일러 코드가 발생할 수 있던 단점 또한 없어졌으니 가세가 기울었다고 볼 수도 있지만, 사용하는 방법 및 상황에 따라 선택하는 것이 현명합니다.

 

Philipe Breault 의 실험 결과에 따르면 Parcelable 이 Serializable 보다 10배 이상 빠르다고 계측되었으며 이것을 많은 사람들이 성능 저하의 근거로 삼고있는데, 이는 어디까지나 기본 사용법에 한해서입니다.

Parcelable 이 직렬화에 대한 정의 코드를 작성해야하는 것처럼 Serializable 에서 자동으로 처리하던 직렬화 프로세스를 사용자가 writeObject/readObject 메서드로 직접 대체한다면 Serializable 에서 기존에 발생하던 Reflection 에 의한 가비지가 더이상 생성되지 않는다고합니다. 이 전제로 계측한 결과 실제로 Serializable 이 Parcelable 보다 쓰기 속도가 3배 이상, 읽기의 경우 1.6배 이상 빨랐습니다.

 

따라서, 위 장단점들을 고려해 목적과 시간, 비용, 개발자의 상황에 맞게 적절히 선택하는 것이 권장됩니다.

 


정리

자바의 Serializable은 런타임에 JVM이 리플렉션으로 클래스의 필드 정보를 읽어 직렬화합니다.

즉, 필드 구조, 타입 등을 실행 도중에 분석해서 직렬화/역직렬화가 이뤄지며, 이로 인해 성능이 떨어집니다.

 

반면, Parcelable은 "컴파일 타임에 직렬화에 필요한 모든 코드가 생성되어" 런타임에는 단순한 readInt(), writeString() 호출만 하게 됩니다.

  • Serializable은 런타임에 "이 클래스에 뭐가 있는지 보자~" 하면서 통째로 훑고 분석해서 직렬화하는 스타일
  • Parcelable은 "컴파일할 때 다 정해놨으니, 그냥 그대로 쓰자!" 하고 정해진 방식대로 빠르게 읽고 쓰는 스타일

 

끝.

참고자료

https://medium.com/@limgyumin/parcelable-vs-serializable-%EC%A0%95%EB%A7%90-serializable%EC%9D%80-%EB%8A%90%EB%A6%B4%EA%B9%8C-bc2b9a7ba810

 

Parcelable vs Serializable , 정말 Serializable은 느릴까?

원문 : “Parcelable vs Serializable”

medium.com

https://best-human-developer.tistory.com/133

 

[Android] Serializable vs Parcelable

익숙한 Serializable과 Parcelable Serializable과 Parcelable, 익숙한 녀석들이다. Bundle에 객체를 담아 Intent와 arguments를 통해 다른 곳으로 전달하려면 객체의 클래스가 둘 중 하나를 구현(상속)해야 한다. 나

best-human-developer.tistory.com

https://work2type.tistory.com/entry/Serializable-Parcelable-%EC%B0%A8%EC%9D%B4

 

Serializable Parcelable 차이

안드로이드에는 Activity, Service, Broadcast Receiver, Contents Provider라는 4대 컴포넌트라는 것이 존재한다. 서로 알아서 소통하면 얼마나 좋을까? 하지만 그렇지 않기 때문에 개발자가 오작교를 놔줘야한

work2type.tistory.com

https://velog.io/@haero_kim/Android-Serializable-vs-Parcelable

 

[Android] Serializable vs Parcelable

액티비티 간 데이터 전달을 위한 두 녀석의 차이점

velog.io

https://yejinson97gaegul.tistory.com/entry/Kotlin-Serializable-%EA%B3%BC-Parcelable

 

[Kotlin] Serializable 과 Parcelable

안드로이드 개발 시, 액티비티/프래그먼트 혹은 복잡한 클래스 간에 데이터들을 전달하기 위해서 Serializable 이나 Parcelable 을 사용하는 것을 본 적이 있을 것이다. 일반적으로 기본 타입들을 사용

yejinson97gaegul.tistory.com

https://medium.com/jaesung-dev/android-%EC%A7%81%EB%A0%AC%ED%99%94%EC%99%80-%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94-18fd04f1c0ed