Parcelable vs. Serializable
문자열 하나 정도라면 Intent에 그냥 넣어도 되지만, 변수가 많아질수록 key를 일일이 관리하기도 어렵고 코드도 더러워진다.
Intent().apply {
putExtra(NOTICE_URL, url)
putExtra(NOTICE_ARTICLE_ID, articleId)
putExtra(NOTICE_CATEGORY, category)
putExtra(NOTICE_POSTED_DATE, postedDate)
putExtra(NOTICE_SUBJECT, subject)
}
여러 개의 값을 전달하려면 Parcelable 또는 Serializable을 사용하는 것이 좋다. 그런데 Parcelable과 Serializable 둘 중 무엇을 써야 하는가?
Serializable
``Serializable``은 어떤 클래스를 직렬화(serialization)할 수 있음을 선언하는 인터페이스이다. 멤버 변수나 함수가 하나도 없으며, 단순히 직렬화할 수 있는 클래스임을 표시하는 역할을 한다. 직렬화란 Java 객체를 바이트 배열로 변환하는 과정을 말한다. 직렬화한 결과물(바이트 배열)은 다른 클래스나 프로그램, 또는 다른 기기에서 사용할 수 있다. 바이트 배열을 Java 객체로 되돌리는 과정은 deserialization(역직렬화?)라고 한다.
Parcelable
``Parcelable`` 역시 어떤 클래스가 parcel(꾸러미)로 변환될 수 있음을 나타내는 마킹 인터페이스이다. Java API로 제공되는 ``Serializable``과 달리 ``Parcelable``은 Android API에 포함되어 있다.
구글이 같은 역할을 하는 ``Serializable``을 놔두고 굳이 ``Parcelable``을 정의한 이유는 무엇일까? 여러 이유가 있겠지만, parcel은 프로세스 간 빠른 의사소통을 위해 만들어졌다. Android 공식 문서에서는 다음과 같이 소개하고 있다.
Parcel is not a general-purpose serialization mechanism. This class (and the corresponding Parcelable API for placing arbitrary objects into a Parcel) is designed as a high-performance IPC transport.
이 설명에 의하면 ``Parcelable``은 ``Serializable``보다 빠른 데이터 교환 수단이라고 이해할 수 있다. 대신 ``Parcelable``은 멤버 함수 2개를 implement해야 하며, ``@JvmStatic`` 멤버 변수 ``CREATOR``를 선언해야 한다. ``Serializable``보다 약간 더 귀찮다.
뭘 써야 할까요
그렇다면 ``Intent``에 ``Parcelable``과 ``Serializable`` 중 무엇을 넣는 게 좋을까? 위의 언급대로라면 성능이 더 좋은 ``Parcelable``을 사용해야 할 것처럼 보인다. 정말 ``Parcelable``이 더 빠를까?
테스트
실제로 테스트해 보자. 테스트에는 다음의 ``TestData`` 클래스를 사용하였다. ``Parcelable`` 때문에 boilerplate 코드가 조금 있다.
data class TestData(
val int: Int,
val float: Float,
val double: Double,
val string: String,
val boolean: Boolean,
) : Serializable, Parcelable {
// Parcelable methods
override fun describeContents(): Int {
return int
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(int)
parcel.writeFloat(float)
parcel.writeDouble(double)
parcel.writeString(string)
parcel.writeBoolean(boolean)
}
companion object {
fun create() = TestData(
int = 1,
float = 0.25f,
double = 0.01,
string = "kuring",
boolean = false,
)
@JvmField
val CREATOR = object : Parcelable.Creator<TestData> {
override fun createFromParcel(parcel: Parcel?): TestData {
return TestData(
parcel?.readInt()!!,
parcel.readFloat(),
parcel.readDouble(),
parcel.readString()!!,
parcel.readBoolean(),
)
}
override fun newArray(p0: Int): Array<TestData> {
return emptyArray()
}
}
}
}
테스트 코드는 다음과 같다. ``TestData``를 ``Serializable`` 또는 ``Parcelable``로 간주하여 parcel에 쓰고 읽는 작업을 5천 번 반복하고, 오버헤드의 영향을 없애기 위해 위의 테스트를 각각 10번씩 반복 측정하였다.
// Unit Test에서 실행하기 위해 Robolectric Test Runner를 사용
@RunWith(RobolectricTestRunner::class)
class SerializableParcelableTest {
val data = TestData.create()
private val repeatCount = 5000
@Test
fun parcelableTest() {
val time = measureNanoTime {
repeat(repeatCount) {
val bytes = Parcel.obtain().run {
writeParcelable(data, 0)
marshall()
}
Parcel.obtain().apply {
unmarshall(bytes, 0, bytes.size)
setDataPosition(0)
readParcelable<TestData>(TestData::class.java.classLoader) // Robolectric에서 deprecated된 함수만 지원
}
}
}
println("Parcelable time: $time")
}
@Test
fun serializableTest() {
val time = measureNanoTime {
repeat(repeatCount) {
val bytes = Parcel.obtain().run {
writeSerializable(data)
marshall()
}
Parcel.obtain().run {
unmarshall(bytes, 0, bytes.size)
setDataPosition(0)
readSerializable() // Robolectric에서 deprecated된 함수만 지원
}
}
}
println("Serializable time: $time")
}
}
결과는 다음과 같다.
놀랍게도 통계적으로 ``Serializable``의 실행 시간이 더 짧다는 결론이 나왔다. Unit Test라서 그럴 수도 있겠지만, 내가 보기에는 Serializable의 성능이 현저하게 낮은 수준은 아닌 것 같다. 성능이 비슷하다면 더 편한 ``Serializable``을 쓰는 게 좋지 않을까?
결론
둘 중 무엇을 써도 상관없으나, 굳이 고르라면 ``Serializable``이 사용하기 더 간단할 듯하다. 구현할 함수가 하나도 없기 때문.
참고문헌
이 글은 아래 실험글에서 영감을 받아 작성하였다.