- Today
- Total
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- github
- suspend
- ai
- 진짜학습지후기
- rxjava
- KotlinInAction
- 인공지능
- androidstudio
- posting
- errorhandling
- Android
- pullrequest
- blog
- 진짜일본어
- 일본어기초
- 책리뷰
- Kotlin
- 책추천
- PR
- jlpt
- GIT
- 안드로이드
- 진짜학습지
- 학습지
- n3문법
- CustomTab
- 일본어문법
- webflux
- coroutine
- 코틀린
코딩하는 개굴이
[Kotlin] Serializable 과 Parcelable 본문
안드로이드 개발 시, 액티비티/프래그먼트 혹은 복잡한 클래스 간에 데이터들을 전달하기 위해서
Serializable 이나 Parcelable 을 사용하는 것을 본 적이 있을 것이다.
일반적으로 기본 타입들을 사용할 수도 있지만 점점 늘어나면서
전달해야하는 내용이 복잡해지기 때문에 그렇게 자주 사용되곤하는데,
평소 “둘 다 전달 방식이고 Serializable 은 성능이 떨어지더라.” 라는
간단하고 얕은 이해만 가지고 있었기에 오늘은 한번 정확하게 짚고 넘어가보고자한다.
직렬화란?
제대로 알기 위해서는 우선 우리는 직렬화가 무엇인지 완전히 이해해야한다.
자바의 직렬화는 자바의 시스템 내부(JVM의 힙/스택과 같은 메모리에 상주)에서 사용되는 객체나 데이터를
외부의 자바 시스템에서도 사용할 수 있도록 바이트 형태로 변환하는 기술과
바이트로 형태의 데이터를 다시 객체로 변환(다시 JVM 으로 상주)하는 기술(역직렬화)을 아울러 통칭한다.
어떻게 직렬화 할까?
직렬화를 위한 기본 조건으로는 두가지가 있다. 자바의 기본 (primitive) 타입이거나 Serializable 인터페이스를 상속받은 객체일 것.
간단하게 예를들어 아래와 같은 객체가 해당할 수 있다.
data class User(
val name: String,
val picUrl: String
) : Serializable
왜 직렬화가 필요한가?
데이터를 서버 등으로부터 받아 올 때 보통 CSV/XML/JSON 등의 형태가 전달되곤 한다.
이때, 이를 역직렬화하여 바로 기존 객체처럼 쓸 수 있게하거나 JVM 메모리에만 상주되어있던 그런 객체들을 영속화 (Persistence) 시켜 시스템이 종료되더라도 없어지지 않게끔 처리해야 할 때 (shared preference / db 등) 직렬화를 사용한다.
Serializable 과 Parcelable
안드로이드에서 복잡한 데이터를 클래스 간에 전달하는 방법은 크게 Serializable 과 Parcelable 이 두가지가 있다. 두 방법 모드 직렬화하여 인텐트에 추가시키는 방법인데, 각각에 대해 알아보도록 하자.
Java Serializable
Serializable 은 Android SDK 가 아닌 표준 Java 의 인터페이스이다.
아까 위에서 예시로 언급된 User 클래스이다. Serializable 은 해당 클래스가 직렬화 대상이라고 알려주는 역할 뿐 어떠한 메서드도 가지지 않는 단순한 마커 인터페이스(Marker Interface) 로 사용하기에 매우 편리하다.
data class User(
val name: String,
val picUrl: String
) : Serializable
그러나, Serializable 은 내부에서 Reflection 을 사용해 직렬화를 처리한다. 다라서, 프로세스 처리 중에 추가 객체를 많이 생성하기도 하며 이들은 가비지 컬렉터의 타켓이 되고 성능 저하 및 배터리 소모를 야기하기도 한다.
직렬화에서 제외하자, Transient
자바 프로그래밍 언어에서 사용되는 예약어로, 이는 변수나 필드를 특정 상황에서 일시적으로 직렬화 대상에서 제외시키는데 사용된다. 이 예약어를 변수나 필드 앞에 붙이면 그 변수나 필드 값은 객체를 직렬화 할 때 저장되지 않고 불러올 때는 저장되지 않았으므로 별도의 처리가 필요하다.
예를들면 아래와 같으며, 역직렬화 시 필드 값은 기본 값으로 초기화된다.
data class User(
val name: String,
val picUrl: String
) : Serializable {
@Transient
private val currentLocation: String = "Korea/Seoul/Gangnam"
}
Reflection 이란?
위에서 Reflection 을 이용해 Serializable 이 직렬화를 처리한다고 언급했는데,
정확히 Reflection 이 무엇인지 와닿지 않는 경우를 대비하여 한번 설명해보려한다.
Reflection 은 구체적인 클래스 타입을 알지 못해도 객체를 통해 클래스 정보를 분석하여 런타임에 메서드/타입/변수들에 접근할 수 있도록 해주는 자바 API 를 말한다. 주로 동적으로 클래스를 사용할 때 즉, 작성 시점에는 어떠한 클래스가 사용될지 정확하게 모르지만 런타임 시점에 가져와 실행해야하는 경우에 사용할 수 있기 때문에 Spring, Java 직렬화, JPA 등에 활용되고 있다.
번외로, Reflection 을 이용해 Singleton 객체를 생성할 경우 다른 객체를 반환해 Singleton 이 깨지는 현상이 발생할 수 있다는 재밌는 내용이 있었는데 주제를 살짝 벗어나니, 다음에 다뤄보도록 하고 자세한 내용은 하위 블로그를 참고할 수 있다.
Android Parcelable
Parcelable 은 Android SDK 에서 제공하는 직렬화 인터페이스로, 내부적으로 Reflection 을 사용하지 않도록 설계되어있다. 따라서, 개발자가 직렬화 처리 방법을 명시해야한다. 그 덕에 성능 상의 이점을 가지게 되었지만 개발자가 직렬화 방법을 처리해야하는 수고가 들기 때문에 단점이 되기도 한다.
data class User(
var name: String?,
var picUrl: String?
) : Parcelable {
constructor(parcel: Parcel) {
parcel.run {
name = readString()
}
}
override fun writeToParcel(dest: Parcel?, flags: Int) {
dest?.writeString(name)
dest?.writeString(picUrl)
}
override fun describeContents(): Int = 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)
}
}
}
기존 Serializable 에서와 동일한 데이터 형태이지만 코드량이 훅 늘어난 것을 볼 수 있다.
지금은 고작 2개의 파라미터지만 늘어 날 수록 Parcelable 을 사용하기 위한 유지보수 및 리소스가 많이 소요될 것이다.
그러나, Kotlin 대안을 마련해 두었으니 바로 Parcelize 이다.
org.jetbrains.kotlin.plugin.parcelize 를 플러그인으로 간단히 추가하고 코드로 돌아가보자.
@Parcelize
data class User(
val name: String,
val picUrl: String
) : Parcelable
클래스에 Parcelize 어노테이션을 추가하고 Parcelable을 implement 하는 것으로 간단히 구현이 가능하다.
단, Parcelize 를 사용하려면 직렬화 된 모든 속성이 기본 생성자 (Primary constructor) 에 선언되어있어야하며, abstract 나 sealed 클래스를 허용하지 않는 점을 유의해야한다.
Parcelize 가 지원하는 타입
- primitive type 과 primitive type 의 박스 타입
- Object 와 Enum
- String, Charsequence
- Exception
- Size, SizeF, Bundle 등
- 모든 타입의 배열, Serializable과 Parcelable 구현체 및 Collection 지원 (List 는 ArrayList 로 매핑, Set은 LinkedHashSet 으로 매핑, Map 은 LinkedHashMap 으로 매핑)
- 지원하는 모든 타입의 Nullable 지원
- 등..
만일 지원하지 않는 클래스 타입이 있을 경우 직접 Parceler 를 이용해 작성하고 객체를 매핑시킬 수도 있다.
자세한 내용은 아래에서 참고하도록 하자.
직렬화에서 제외하자, IgnoredOnParcel
Parcelize 를 사용시에도 Serializable 의 Transient 처럼 멤버의 속성을 직렬화 대상에서 제외할 수 있다.
IgnoredOnParcel 을 사용하며, 역직렬화 시 필드 값은 기본 값으로 초기화된다.
@Parcelize
data class User(
val name: String,
val picUrl: String
) : Parcelable {
@IgnoredOnParcel
private val currentLocation: String = "Korea/Seoul/Gangnam"
}
어느 것을 사용해야하는가?
많은 곳에서 Serializable 이 성능적으로 저하를 유발할 수 있기에 Parcelable 을 권장하는 것을 볼 수 있다. Kotlin 의 Parcelize 지원으로 이제 보일러 코드가 발생할 수 있던 단점 또한 없어졌으니 가세가 기울었다고 볼 수도 있지만, 사용하는 방법 및 상황에 따라 선택하는 것이 현명하다.
Philipe Breault 의 실험 결과에 따르면 Parcelable 이 Serializable 보다 10배 이상 빠르다고 계측되었으며 이것을 많은 사람들이 성능 저하의 근거로 삼고있는데, 이는 어디까지나 기본 사용법에 한해서이다.
Parcelable 이 직렬화에 대한 정의 코드를 작성해야하는 것처럼 Serializable 에서 자동으로 처리하던 직렬화 프로세스를 사용자가 writeObject/readObject 메서드로 직접 대체한다면 Serializable 에서 기존에 발생하던 Reflection 에 의한 가비지가 더이상 생성되지 않는다고한다. 이 전제로 계측한 결과 실제로 Serializable 이 Parcelable 보다 쓰기 속도가 3배 이상, 읽기의 경우 1.6배 이상 빨랐다.
따라서, 위 장단점들을 고려해 목적과 시간, 비용, 개발자의 상황에 맞게 적절히 선택하는 것을 권장한다. :)
참고 링크
- https://techblog.woowahan.com/2550/
- https://golf-dev.tistory.com/m/48
- https://medium.com/kenneth-android/kotlin-serializable와-parcelable-차이-그리고-kotlin-pacelize-f78dc6c3a208
- https://tourspace.tistory.com/360
- https://developer-jp.tistory.com/97
- https://www.charlezz.com/?p=44613
- https://medium.com/@limgyumin/parcelable-vs-serializable-정말-serializable은-느릴까-bc2b9a7ba810
- https://blacktrees.tistory.com/entry/Android-Parcelable과-Serializable의-차이점
- https://bladecoder.medium.com/a-study-of-the-parcelize-feature-from-kotlin-android-extensions-59a5adcd5909
- https://brunch.co.kr/@oemilk/179
'안드로이드 > KOTLIN' 카테고리의 다른 글
[Kotlin] Data class, equals 를 재정의 할 거면 hashCode는 ? (0) | 2024.03.11 |
---|---|
[Kotlin] Coroutine, 잠시 멈춰! (0) | 2024.03.03 |
[Android] Jetpack Compose 한 입 찍먹하기(List/Navigation/Dialog) (0) | 2024.01.21 |
코드로 Coroutine 동작 파악하기 (0) | 2023.09.17 |
[Kotlin] Coroutine Dispatchers.Main 의 동작 순서 보장 (feat. Dispatchers.Main.immediate) (1) | 2023.06.18 |