안드로이드 앱 개발자라면, 당연하게도 사용할 수 밖에 없는 두 녀석이다. 액티비티에서 액티비티로 '객체'를 전달하기 위해서는, Serializable
혹은 Parcelable
을 활용하여 Intent Extra
를 넣어줘야 하기 때문이다. 이를 몰랐을 땐 아래와 같은 과정을 겪었을 것이다.
"그렇다고 필드값 하나 하나를 일일히 putExtra(String)
이나 putExtra(Int)
등으로 넘겨주기엔 구현이 너무 더러운 것 같고, 한 번에 객체 자체를 통째로 넘기는 방법은 없을까?"
하며 아래와 같이 구글링을 했을 것이다.
그러면 아래와 같은 검색결과가 나왔을 것이다.
Serializable
을 활용한 객체 전달 . . . 뭐시기 . . .Parcelable
을 사용하여 객체 전달하기 . . . 뭐시기 . . .엥 방법이 두 가지나 있다고? 하고 글을 하나씩 본 당신이 든 생각은 다음과 같았을 것이다.
뭐야,
Parcelable
뭐가 이렇게 어렵고 복잡해?Serializable
개 편하네 ㅋㅋ 이거 써야지
하고서는 인텐트로 넘길 객체 클래스에 Serializable
인터페이스를 구현해서 객체를 전달했을 것이다.
그런데 무지성으로 Serializable
써왔지만, 이게 뭐하는 녀석인진 알고 써왔던 사람이 몇이나 될까! (필자도 그 중 하나였다) 단순히 Parcelable
의 구현이 복잡하다고 Serializable
만 사용해왔다면 반성할 필요가 있다.
따라서 이번 포스팅에선 이들 각각의 특징을 살펴보고, 무엇이든 정확히 알고 사용하는 개발자가 되기 위해 한걸음 나아가는 시간을 갖도록 하자.
Serial + able .. 직렬화 가능한.. 맞다. 우리가 아는 그 '직렬화' 가 가능하게끔 만들어주는 인터페이스이다.
💡 직렬화란?
메모리 상 정보를 Byte 단위의 코드를 통해 직렬로 나열하는 것
Serializable
은 Java 의 표준 인터페이스이다. 따라서 Android SDK 에 포함되어있는 녀석은 아니다. 다만 Intent 로 객체 데이터를 넘기기에 목적과 용도가 일치하기 때문에 종종 사용되곤 한다.
Serializable
은 다음과 같은 장단점을 지닌다.
Serializable
인터페이스 상속받으면 끝)Reflection
을 사용하기 때문)🤷🏻♂️ Reflection 이 뭔데요 ㅋㅋ
Reflection 은 자바에서 기본적으로 제공하는 API 이다. 생성되어 있는 객체를 통해 해당 객체의 클래스 정보를 분석해내는 기법을 뜻한다. (클래스 명, 접근 지정자, 패키지 정보, 슈퍼 클래스, 구현한 인터페이스, 생성자, 메소드, 필드, 어노테이션 등의 정보 활용) Reflection 은 사용 당시 객체의 구체적인 클래스 형태를 모를 때 사용하게 된다.
따라서, 객체의 속성(필드값 등)이 상황에 따라 언제든 변경될 수 있는 상황에서 사용한다. Intent
로 넘기는 객체 정보 역시, 앱 실행 도중 동적으로 생성된 객체를 넘기는 경우가 많으니 리플렉션을 사용하게 된다.
이렇게 Reflection 을 활용하게 되면, 상당히 속도가 느려지게 된다. 런타임 시 데이터를 직렬화 하고 역직렬화하는 과정에서 많은 객체를 생성하게 되고, 이 말은 즉슨 GC 의 부담이 커진다는 뜻이고, 이 말은 또 CPU 의 부담이 커진다는 뜻이기 때문이다. 정보가 많아질 수록 성능에 악영향을 끼치게 되는 것이다.
(이 포스팅에선 Reflection 에 대해서 자세히 다루지 않겠다)
Parcelable
은 Android 에서 지원해주는 SDK 에 포함되어 있는 인터페이스이다. Parcel 은 소포, 택배라는 뜻을 가지고 있다. Parcelable 이라 함은 택배로 부칠 수 있는,, 정도로 해석할 수 있겠다.
Parcelable
은 다음과 같은 장단점을 갖는다.
Serializable
대비 훨씬 빠른 속도 (Reflection 사용 X, 직렬화와 역직렬화 로직을 개발자가 구현해주기 때문에, 이러한 로직들이 미리 컴파일되어 런타임 시 빠른 처리가 가능하다)대신, 직렬화와 역직렬화 로직을 개발자가 직접 구현해야 한다는 말에서 알 수 있듯 단점을 유추해볼 수 있다.
writeToParcel()
메소드를 통해 Parcel 객체에 데이터 저장createFromParcel()
메소드를 통해 Parcel 로부터 원래 객체 모양에 따라 데이터 복원만약 이름과 이메일 필드를 갖는 User 라는 클래스가 있다고 하자. 아래와 같은 모양일 것이다.
class User {
val name: String? = null
val email: String? = null
}
이 클래스에 Parcelable
인터페이스를 구현하자면 다음과 같은 모양이 된다.
import android.os.Parcel
import android.os.Parcelable
class User() : Parcelable {
var name: String? = null
var email: String? = null
constructor(parcel: Parcel) : this() {
parcel.run {
name = readString()
email = readString()
}
}
override fun writeToParcel(dest: Parcel?, flags: Int) {
dest?.run {
writeString(this@User.name)
writeString(this@User.email)
}
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<User> {
override fun createFromParcel(parcel: Parcel): User {
return User(parcel)
}
override fun newArray(size: Int): Array<User?> {
return arrayOfNulls(size)
}
}
}
숨이 턱 막히는데요..?
Parcelable
인터페이스를 사용하면 보일러 플레이트 코드가 상당해져서 솔직히 귀찮다.
시대가 흐를수록 하드웨어 성능도 좋아졌기 때문에 진짜 미세한 차이로 Parcelable
이 빠른데, 위와 같은 보일러 플레이트 코드를 구현함으로써 얻는 이점이 그리 크진 않은 듯 하다. 직렬화/역직렬화 로직을 구현할 시간에 다른 핵심 로직에 더욱 집중하는 것이 낫지 않을까 하는 생각이다.
필자와 같은 생각을 담은 아티클을 발견하여 URL을 첨부한다.
https://medium.com/@limgyumin/parcelable-vs-serializable-정말-serializable은-느릴까-bc2b9a7ba810
위에서 살펴본 Serializable
과 Parcelable
은 각기다른 장단점을 지니고 있다. Serializable
은 속도가 느린 대신 구현이 간편하다는 장점이 있고, Parcelable
은 구현이 복잡한 대신 속도가 빠르다는 장점이 있다. 분명 인텐트 시 객체 전달은 앱 개발 시 꽤나 자주 사용되는데, 이들의 장점만을 합쳐볼 순 없을까 하여 탄생하게 된 'Parcelize' 가 있다.
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
의 속도면에서의 장점을 모두 챙긴 것이다!
💡 공식문서
이번 포스팅에선 Serializable
과 Parcelable
각각의 특징과 장단점에 대해서 알아보았고, 이들의 발전 형태인 Parcelize
플러그인에 대해서도 알아보았다. 매번 뭐하는 녀석인지도 모르고 사용하기보다, 내가 사용하는 것이 어떤 원리로 동작하는지 알 필요가 있다. 상황에 맞게, 용법에 맞게 이를 적절히 선택하여 활용할 수 있도록 하자.
굿굿!