직렬화란 무엇이고, 왜 필요하며, 어떻게 직렬화를 할 수 있을까?
DoDoBest
·2024. 4. 18. 01:23
직렬화란?
직렬화란 Application에서 사용하는 데이터를 네트워크를 통해 전송할 수 있는 형태, 데이터베이스나 파일에 저장할 수 있는 형태로 변환하는 작업을 의미합니다. 역직렬화란 외부 소스로부터 읽어온 데이터를 runtime Object로 변환하는 작업을 의미합니다. 직렬화 포맷으로 자주 사용되는 형태는 JSON, Protocol buffers가 있습니다. 그 외에 CBOR, Properties, HOCON 등이 있습니다.
직렬화를 해주는 이유는?
만약 Android와 IOS 간에 통신을 할 때, Android에서 사용하는 데이터 클래스 형태 그대로 IOS에게 전달하면, IOS는 수신한 바이트코드만으로는 데이터를 해석할 수 없습니다. 그래서 Android와 IOS가 통신하기 전에 서로 약속된 형태(그게 JSON이나 Protocol Buffer가 될 수 있습니다.)로 변환해서 데이터를 주고 받자고 규칙을 정해야 합니다. 정해진 규칙에 따라 데이터를 변환하는 작업이 직렬화입니다.
즉, 외부 시스템에서 이해할 수 있는 데이터로 변환이 필요하기 때문에 직렬화를 해주는 겁니다.
그런데 Fragment와 Fragment, Activity와 Activity는 같은 Android 시스템에 속하기 때문에, Bundle에 포함할 데이터를 직렬화하지 않아도 되는 것 아닌가요?
아닙니다! Intent는 내가 작성한 앱이 아닌 다른 앱에게도 전달될 수 있습니다. 다른 앱은 내가 작성한 앱에 있는 데이터 정보를 모르기 때문에, 직렬화하지 않은 데이터의 바이트 코드만 봐서는 무엇을 의미하는지 알 수 없습니다!
더 원초적인 예시를 들어보면, C언어에서 String은 Char 배열로 되어 있습니다. Char 배열의 시작 또는 끝을 알 수 없으면, 내가 원하는 데이터가 무엇을 의미하는지 알 수 없습니다.
따라서 서로가 이해할 수 있는 데이터로 변환하는 약속인 직렬화 작업은 꼭 필요합니다!
JSON과 Protocol Buffer
JSON -> Key-value Pair로 구성된 String 형태
Protocol Buffer -> Byte 단위로 주고 받는 데이터
JSON은 컴퓨터가 이해할 수 있는 Byte로 변환하는 작업이 필요하기 때문에 Protocol Buffer보다 처리 속도가 느립니다.
하지만 JSON은 사람이 이해할 수 있는 String으로 되어 있기 때문에, 데이터를 주고 받는 과정에서 오류가 발생했을 때, 일부 수신한 JSON 데이터를 읽을 수 있다는 장점이 있습니다.
Intent란 무엇인가?
Intent는 다른 app component(Activity, Service, Broadcast Receiver, Content Provider)에게 요청할 동작을 담은 Message 객체(object)입니다.
Intent는 명시적(Explicit) Intent와, 암시적(Implicit) Intent가 있습니다.
명시적 Intent
요청할 동작을 수행할 App Component 이름을 구체적으로 명시합니다. 예를 들어, 같은 앱에 있는 Activity 또는 Service 이름을 알 수 있기 때문에, 아래와 같이 구체적으로 명시하여 Intent를 생성하면 Android System은 해당 App Component에게 Intent를 전달합니다.
val intent = Intent(this, TargetActivity::class.java).apply {
putExtra(TargetActivity.BUNDLE_KEY_FOR_NAME, "Hello")
}
startActivity(intent)
암시적 Intent
요청할 동작을 수행할 App Component 이름을 명시하지 않고, 원하는 동작을 의미하는 값을 명시합니다.
동작을 의미하는 값은 Intent 클래스 내부에 ACTION_XXX 형태로 정의되어 있습니다.
예를 들어, 사용자에게 보여줘야 하는 데이터가 있을 때 사용하는 ACTION_VIEW와 인터넷 주소를 의미하는 URI를 Intent에 담아서 실행하면, Android 시스템은 intent filter로 인터넷 주소 처리를 등록한 Component를 실행하고, 해당 Component에게 Intent를 전달합니다. 처리할 수 있는 Component가 다수라면 사용자가 선택할 수 있도록 Dialog를 보여줍니다. 처리할 수 있는 Component가 없으면 RuntimeException이 발생하기 때문에 주의해야 합니다.
intent filter는 Component가 수신하고 싶어하는 intent를 의미하며, manifest 파일에 기재할 수 있습니다.
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("http://www.google.com"))
startActivity(intent)
ACTION_VIEW와 전화번호를 의미하는 tel:XXX를 명시하면 전화를 할 수 있는 Component가 Intent를 수신합니다.
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("tel:010-0000-0000"))
startActivity(intent)
Intent에 전달할 데이터를 포함하는 방법
Intent로 요청하는 행위를 수행하기 위해 필요한 데이터가 있다면 Extras를 이용해 key-value 형태로 전달할 수 있습니다.
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("tel:010-0000-0000")).apply {
putExtra("Key", "value")
}
startActivity(intent)
또는 Bundle을 이용할 수 있습니다. Bundle은 Parcelable한 데이터를 key-value 형태로 가지고 있는 class로, parcel을 이용해 marshalling과 unmarshalling에 매우 최적화 되어 있습니다.
marshalling이란 application 단의 고수준(high level) 데이터를 parcel 형태로 변환하는 과정을 의미합니다.
unmarshalling이란 parcel로부터 고수준 데이터를 만드는 과정을 의미합니다.
val bundle = Bundle().apply {
putString("key", "value")
}
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("tel:010-0000-0000")).apply {
putExtra("Bundle Key", bundle)
}
startActivity(intent)
Bundle을 사용하든, Intent에 직접 putExtra를 하는지와 무관하게 입력하는 데이터는 직렬화가 가능해야 합니다.
직렬화가 필요한 이유는 위에서 설명했기 때문에 생략하겠습니다!
직렬화 방법 1. Serialize
객체를 Serialize 한다는 것은 객체의 상태(object's state)를 byte stream으로 변환하는 것을 의미합니다.
deserializing은 byte stream으로부터 객체를 복원한다는 것을 의미합니다.
전달하고자 하는 객체가 Serializable 인터페이스를 구현하도록 설정하면 컴파일 과정에서 JVM이 자동으로 직렬화 처리를 해줍니다.
import java.io.Serializable
data class UserInfo(
val name: String,
val age: Int,
): Serializable
Serializeable은 내부가 비어 있는 인터페이스 입니다. 이러한 인터페이스를 마커 인터페이스(Marker Interface)라고 부릅니다.
Serialize의 단점은 역직렬화 과정에서 reflection을 이용한다는 점인데, 이 과정에서 임시 객체를 다수 생성해서 메모리를 차지하며, 역직렬화를 잘못하면 hashCode 메서드를 계속 호출해야 하므로 시간이 오래 걸립니다.
직렬화 방법 2. Parcelable
Parcelable은 Binder, IPC(inter-process-communiation) transport 과정에 최적화된, 데이터 또는 객체를 담는 Container 입니다.
Parcel은 JSON과 같은 일반적인 목적의 serialization mechanism이 아닙니다. 따라서 Parcel 데이터를 disk에 저장하거나 네트워크 통신에 사용해서는 안 됩니다. Android에서 사용하는 특수한 형태입니다.
Parcelize를 위해서는 Parcelable 인터페이스를 구현해야 하며, 직렬화와 역직렬화 과정을 직접 작성해야 합니다. writeToParcel 함수는 직렬화 과정을 담고 있으며, createFromParcel 함수를 통해 역직렬화가 이루어집니다.
data class UserInfo(
val name: String,
val age: Int,
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readInt()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name)
parcel.writeInt(age)
}
// Parcelable에 특별한 종류의 데이터가 포함되어 있다면 해당 데이터 타입을 의미합니다.
// ex) CONTENTS_FILE_DESCRIPTOR
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<UserInfo> {
override fun createFromParcel(parcel: Parcel): UserInfo {
return UserInfo(parcel)
}
override fun newArray(size: Int): Array<UserInfo?> {
return arrayOfNulls(size)
}
}
}
Parcelize는 직렬화, 역직렬화 과정을 직접 구현하기 때문에 Reflection을 사용하지 않습니다. 그래서 Serialize보다 약 10배 가량 빠릅니다. (출처: https://www.developerphil.com/parcelable-vs-serializable/ )
Parcelize 간단하게 처리하기
Parcelize 인터페이스를 상속해서 매번 코드를 작성하기란 번거롭습니다.
안드로이드에서는 애노테이션을 통해 자동으로 Parcelize 코드를 생성해주는 플러그인을 제공합니다.
app 모듈 수준의 build gradle plugin에 kotlin-parcelize를 추가해줍니다.
plugins {
id("kotlin-parcelize")
}
그러면 @Parcelize 애노테이션을 사용할 수 있습니다.
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class UserInfo(
val name: String,
val age: Int,
) : Parcelable
안드로이드 공식 문서에서 process 간에 데이터를 전달할 때, custom parcelable을 사용하지 않는 것을 권장합니다.
Sending data between processes is similar to doing so between activities. However, when sending between processes, we recommend that you do not use custom parcelables. If you send a custom
Parcelable
object from one app to another, you need to be certain that the exact same version of the custom class is present on both the sending and receiving apps.
추가로 AlarmManager를 예시로 설명 해줬는데, AlarmManager를 직접 구현해보고 별도의 글을 작성한 후 링크를 첨부하도록 하겠습니다.
https://developer.android.com/guide/components/activities/parcelables-and-bundles#sdbp
Binders와 IPC
IPC는 Inter Process Communication의 약자로, 두 application 또는 두 프로세스 간에 데이터를 주고 받는 mechanism을 의미합니다.
Android는 임베디드나 작은 소형 기기에 주로 사용되기 때문에, 성능적인 면에서 serialization 대신 Binders를 IPC로 사용해야 합니다. Binders는 내부에서 parcel을 사용합니다.
Binders는 Android에 특화된 IPC mechanism Framework입니다. Binders를 이용하면, 다른 프로세스(Activity, Service 등)에 대한 참조를 갖을 수 있습니다. 그래서 다른 프로세스에 있는 함수를 local method를 호출하는 것처럼 사용하도록 해줍니다.
참고 자료
https://kotlinlang.org/docs/serialization.html
https://blog-tech.tadatada.com/2019-05-08-tada-client-development
https://developer.android.com/guide/components/intents-filters
https://proandroiddev.com/serializable-or-parcelable-why-and-which-one-17b274f3d3bb
https://developer.android.com/guide/components/activities/parcelables-and-bundles
https://developer.android.com/kotlin/parcelize
https://stackoverflow.com/a/35450853/11722881
'학습' 카테고리의 다른 글
FrameLayout에서 Fragment 올바르게 사용하기 (0) | 2024.05.02 |
---|---|
Fragment에서 ViewPager2 + TabLayout 사용하기 (0) | 2024.04.24 |
Android Task와 ACTIVITY FLAG 정복하기 (0) | 2024.04.07 |
Android 이미지 읽기 권한 다루기 (0) | 2024.04.02 |
ViewModelProvider, ViewModelStore (0) | 2024.03.29 |