(Android) 직렬화와 역직렬화

Serializable, Parcelable 선택 기준

Jaesung Lee
jaesung dev
9 min readOct 5, 2023

--

Photo by James Harrison on Unsplash

안드로이드 개발을 하다보면 화면 간 데이터를 전달할 일이 자주 발생합니다. 처음 안드로이드를 학습할 때를 떠올려보면 안드로이드 컴포넌트 간에는 Intent를 이용하여 통신할 수 있었고, 데이터 전달이 필요할 경우에는 Bundle 객체를 통해 데이터를 전달할 수 있었습니다.

화면 설계를 하다보면 간단한 값(value)만 전달하는 것이 아닌 객체(object)를 전달해야 할 경우도 생깁니다. 이 경우에는 직렬화가 필요합니다. 이번 글에서는 사용 기술 복기를 겸해 직렬화와 역직렬화에 대해 정리합니다.

직렬화 (Serialization)와 역직렬화 (Deserialization)

Serialization is a mechanism of converting the state of an object into a byte stream. Deserialization is the reverse process where the byte stream is used to recreate the actual Java object in memory.
- https://www.geeksforgeeks.org/serialization-in-java/

위 원문에서도 설명하듯이 직렬화는 객체 또는 데이터를 외부의 시스템에서도 사용할 수 있도록 byte-stream 형태로 변환하는 기술을 의미합니다. 역직렬화는 그 반대 과정으로 byte-stream 형태의 데이터를 다시 원본 객체로 변환하는 기술을 의미합니다.

직렬화를 사용하는 이유

학부시절 운영체제 수업 때 배웠던 내용을 떠올려보면, 각각의 프로세스는 별도의 메모리를 갖기 때문에 프로세스 간 데이터 공유를 위해서는 별도의 기법이 필요했습니다. 이 기법을 IPC (Inter-Process Communication)라고 배웠습니다.

안드로이드에서는 프로세스 간 통신을 지원하는 Binder를 사용할 수 있습니다. IPC를 통해 우리는 다른 프로세스에 byte-stream을 전달할 수 있고, 이러한 byte-stream을 만들기 위해 직렬화가 필요하다고 볼 수 있습니다.

java.io.Serializable

Java에서는 아래 조건을 만족할 경우 직렬화가 가능하다고 판단할 수 있습니다.

  • 기본 (Primitive) 타입
  • java.io.Serializable 인터페이스를 구현한 객체

여기서 Serializable 인터페이스는 Android 의존성을 갖지 않는 표준 Java의 인터페이스입니다. 특이한 점은, 이 인터페이스에는 별도로 구현해야 하는 메서드가 없습니다. 이러한 인터페이스를 마커 인터페이스 (Marker Interface)라고 부릅니다.

직렬화가 필요한 객체에 단순히 Serializable의 구현만 명시해준다면 매우 쉽게 사용 가능합니다. 이 경우, JVM 내부에서 자동 직렬화, 역직렬화 프로세스를 통해 처리하게 됩니다.

직렬화 과정

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

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

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

역직렬화 과정

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

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

serialVersionUID (SUID)

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

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

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

Serializable의 단점

1. 안전하지 못한 SUID

SUID를 수동으로 직접 명시한다 하더라도 완벽하다고는 볼 수 없습니다. 직렬화 할 객체에 새로운 필드가 추가되는 것이 아닌 타입이 변경될 경우에도 예외 (InvalidClassException)는 발생하게 됩니다.

2. 역직렬화 시 발생되는 과도한 Reflection

역직렬화 할 객체에 대해 ObjectInputStream#readOrdinaryObject를 수행하고 내부적으로는 Reflection을 통해 불필요한 객체들이 생성되게 됩니다. 이러한 불필요한 객체들은 과도한 GC의 발생 원인이 될 수 있기 때문에 성능적으로는 좋지 못합니다. 전체적인 코드의 흐름은 아래를 참고하시면 됩니다.

  • ObjectInputStream#readObject0
  • ObjectInputStream#readOrdinaryObject
  • ObjectInputStream#readSerialData
  • ObjectStreamClass#invokeReadObject
  • Object#invoke

물론 이러한 방법은 개발자가 직접 writeObject, readObject를 구현하여 개선할 수 있습니다. Reflection에 의한 성능 저하는 앞서 설명한 자동 직렬화/역직렬화 프로세스에 의한 현상이기 때문에 직접 구현하여 개선할 수 있습니다.

Android Parcelable

Parcelable도 Serializable과 마찬가지로 직렬화에 쓰일 수 있는 인터페이스입니다. 이름에서도 알 수 있듯이 Parcelable은 순수 언어 의존성을 갖지 않고 Android 의존성을 갖고 있습니다.

Parcelable 인터페이스에는 직렬화 처리 방법을 개발자가 직접 명시하여 작성할 수 있도록 메서드를 제공하고 있습니다. 따라서, Serializable과 달리 자동으로 처리되지 않으며 이에 따른 Reflection도 존재하지 않습니다.

직렬화 과정

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

역직렬화 과정

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

@Parcelize Annotation

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

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

어떻게 쓰는게 좋을까?

Serializable과 Parcelable의 퍼포먼스를 비교하는 대표적인 글에서 말하듯, 두 직렬화 방식의 실질적인 성능 차이는 크지 않습니다. Serializable은 자동 직렬화 메커니즘을 따라 Reflection이라는 큰 단점이 존재하긴 하지만, Parcelable 처럼 개발자가 직접 직렬화 로직을 정의할 수 있기 때문에 Serializable이 더 느리다고 판단할 수 없습니다.

실질적인 성능 차이가 거의 존재하지 않는 두 직렬화 방식을 어떻게 사용하면 좋을지에 대한 고민을 해봤습니다. 레이어 간의 의존성을 완전히 분리하고 이에 따라 도메인 모델과 UI 모델을 나누게 된다면 여기서 사용법을 생각해볼 수 있습니다.

만약, 도메인 모델에 대한 직렬화가 필요하다면 안드로이드 의존성을 갖는 Parcelable 보다는 Serializable 사용이 더 적절합니다. 하지만, 화면간 객체 전달을 위해 직렬화를 사용한다면 도메인 모델에 직렬화의 책임을 주는 것 보다 UI 모델에 직렬화의 책임을 주는 것이 적절해 보입니다. 따라서, UI Model에서는 Parcelable을 사용하는 것이 좋을 것 같습니다.

또한, 서두에 설명한 것 처럼 화면 간의 객체 전달을 위해 Intent를 사용하게 되는데, Intent에서는 객체 전달을 위해 아래와 같은 메서드들을 제공하고 있습니다.

  • Intent#putExtra(String, Parcelable)
  • Intent#putParcelalbeArrayListExtra(String, ArrayList<out Parcelable>)

Serializable은 이러한 리스트 형태의 전달이 불가능하지만, Parcelable은 기본으로 제공하고 있습니다. 편의성을 생각해본다면 Presentation Layer에서는 더더욱 Parcelable의 사용이 적절해 보입니다.

--

--