코딩하는 개굴이

[Kotlin] Serializable 과 Parcelable 본문

안드로이드/KOTLIN

[Kotlin] Serializable 과 Parcelable

개굴이모자 2024. 2. 4. 19:08
반응형

안드로이드 개발 시, 액티비티/프래그먼트 혹은 복잡한 클래스 간에 데이터들을 전달하기 위해서

Serializable 이나 Parcelable 을 사용하는 것을 본 적이 있을 것이다.

 

일반적으로 기본 타입들을 사용할 수도 있지만 점점 늘어나면서

전달해야하는 내용이 복잡해지기 때문에 그렇게 자주 사용되곤하는데,

평소 “둘 다 전달 방식이고 Serializable 은 성능이 떨어지더라.” 라는

간단하고 얕은 이해만 가지고 있었기에 오늘은 한번 정확하게 짚고 넘어가보고자한다.

 

직렬화란?

제대로 알기 위해서는 우선 우리는 직렬화가 무엇인지 완전히 이해해야한다.

자바의 직렬화는 자바의 시스템 내부(JVM의 힙/스택과 같은 메모리에 상주)에서 사용되는 객체나 데이터를
외부의 자바 시스템에서도 사용할 수 있도록 바이트 형태로 변환하는 기술과
바이트로 형태의 데이터를 다시 객체로 변환(다시 JVM 으로 상주)하는 기술(역직렬화)을 아울러 통칭한다.

이미지 출처, 참고링크 https://brunch.co.kr/@oemilk/179

 

 

어떻게 직렬화 할까?

직렬화를 위한 기본 조건으로는 두가지가 있다. 자바의 기본 (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..?

 

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배 이상 빨랐다.

 

따라서, 위 장단점들을 고려해 목적과 시간, 비용, 개발자의 상황에 맞게 적절히 선택하는 것을 권장한다. :)

 

 

참고 링크

반응형
Comments