Primary/Android

Parcelable vs. Serializable

해스끼 2023. 7. 22. 21:11

문자열 하나 정도라면 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``이 사용하기 더 간단할 듯하다. 구현할 함수가 하나도 없기 때문.

참고문헌

 

Serializable (Java SE 17 & JDK 17)

All Known Subinterfaces: Attribute, Attribute, Attributes, CertPathValidatorException.Reason, Connector.Argument, Connector.BooleanArgument, Connector.IntegerArgument, Connector.SelectedArgument, Connector.StringArgument, Control, Descriptor, DHPrivateKey,

docs.oracle.com

 

 

Parcel  |  Android Developers

 

developer.android.com

 

Parcelable  |  Android Developers

 

developer.android.com

이 글은 아래 실험글에서 영감을 받아 작성하였다.

 

Parcelable vs Serializable

Often, when we develop applications, we have to transfer data from one Activity to another. Of course, we can not do that directly. The…

medium.com