본문 바로가기

Android/Tech

Serializable, Parcelable

Unsplash, Anne Nygard.

개발을 하다 보면 자료를 직렬화해야 하는 경우가 종종 있습니다. 안드로이드 개발의 경우는 Intent 를 통해 Activity 에서 또 다른 Activity 로 데이터를 보낼 때 이용됩니다. Intent 의 putExtra() 메서드는 Primitive 타입 (코틀린엔 Primitive 타입이 없지만) 들을 메서드 오버로딩을 통해 구현해 두었기 때문에 별도의 직렬화가 필요하지 않습니다. 하지만 그 외의 클래스 인스턴스를 전송하려면 직렬화가 요구됩니다.

 


직렬화

직렬화는 데이터를 디스크에 저장하거나 네트워크 통신에 사용하기 위한 형식으로 변환하는 것입니다.

반댓말은 역직렬화입니다. 역직렬화는 당연히, 디스크에 저장한 데이터를 읽거나, 네트워크 통신을 통해 받은 데이터를 메모리에 쓸 수 있도록 변환하는 것입니다.

 

🤔 직렬화는 왜 필요한가요?

데이터는 값 형식참조 형식으로 나뉩니다. 먼저 값 형식 데이터Stack 영역에 메모리가 적재되고 직접 접근이 가능한 데이터들이며, 참조 형식 데이터는 해당 형식의 인스턴스를 선언하면 Heap 영역에 메모리가 적재되고, Stack 영역에서는 해당 인스턴스의 메모리 주소를 참조하는 구조를 갖는 데이터입니다.

값 형식과 참조 형식 중에서, 디스크에 저장하거나 통신에 사용하는 데이터는 값 형식 데이터입니다.

참조 형식 데이터는 실제 데이터 값이 아니라 Heap 에 할당된 메모리 번지 주소만을 갖고 있기 때문에, 저장 및 통신에 사용할 수 없습니다.

 

직렬화를 수행하면, 참조 형식 데이터를 값 형식 데이터로 변환하여, 디스크 쓰기 및 통신에 해당 데이터를 사용할 수 있게 되는 것입니다.

 


안드로이드에서의 직렬화

안드로이드에서의 직렬화를 수행하려면 두 가지 선택지 중 하나를 선택하면 됩니다.

  • Serializable
  • Parcelable

Serializable 은 Java 에 구현되어 있고, Parcelable 은 AndroidSDK 에 구현되어 있습니다.

성능에 대한 설전이 많았는데, Parcelable 이 Serializable 보다 빠르고 메모리도 적게 사용한다는 것이 정설인 듯 합니다.

 


Serializable

Java 가 제공하는 직렬화 인터페이스입니다. 사용법은 너무나도 간단합니다.

 

data class AdBanner(
    val createDateTime: String,
    val id: Int,
    val image: String
) : Serializable

 

데이터 클래스가 Serializable 인터페이스를 구현하도록 해주면 끝입니다.

사용법이 굉장히 쉬운데, 사용이 쉽다는 것은 그만큼 추상화가 잘 되어 있다는 것을 의미합니다.

Serializable 은 내부적으로 Java 의 Reflection 을 이용하도록 구현되어 있습니다.

 

🤔 Refelction 이 뭔가요?

Reflection 은 java.lang.reflect 에 구현된 API 로, 제네릭 사용 등의 이유로 명시되지 않은 클래스에 대한 정보를 해당 클래스의 인스턴스를 직접 생성하여 획득할 수 있습니다. 생성자나 메서드, 필드 모두 얻을 수 있으며, 참조 및 호출, 그리고 수정까지 가능합니다.

 

즉, Serializable 을 구현하는 클래스가 직렬화될 때, 내부적으로 수많은 Reflection 메서드가 실행되게 되고 사용하지 않는 인스턴스까지 생성하게 되면서 수많은 비용의 낭비가 발생합니다.

 

즉, Serializable 은 사용하기 쉬운 대신 시스템에 부하를 줍니다.

 


Parcelable

AndroidSDK 에 구현된 직렬화 인터페이스입니다. 사용법이 조금 복잡합니다.

Parcelable 인터페이스를 구현하도록 하면 되는데, 딸린 멤버의 양이 적지만은 않습니다.

 

data class AdBanner(
    val createDateTime: String?,
    val id: Int,
    val image: String?
) : Parcelable {
    constructor(parcel: Parcel) : this(
        parcel.readString(),
        parcel.readInt(),
        parcel.readString()
    ) 

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(createDateTime)
        parcel.writeInt(id)
        parcel.writeString(image)
    }

    override fun describeContents(): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<AdBanner> {
        override fun createFromParcel(parcel: Parcel): AdBanner {
            return AdBanner(parcel)
        }

        override fun newArray(size: Int): Array<AdBanner?> {
            return arrayOfNulls(size)
        }
    }
}

 

Parcelable 은 Serializable 과 달리 Reflection 을 이용하지 않습니다. 어떻게 Reflection 을 이용하지 않는지는 코드를 보면 알 수 있습니다. 바로 오버라이드되는 writeToParcel() 메서드 덕분입니다. 해당 메서드는 클래스의 필드를 Parcel 객체에 직접 작성해주는 메서드입니다. Serializable 을 구현한 클래스였다면 Reflection 을 통해 클래스와 필드를 가져오기 위해 고군분투했겠지만, Parcelable 을 구현했기 때문에, 개발자가 작성해 둔 코드를 통해 순식간에 Parcel 객체에 필드를 넣어줍니다.

 


그럼 Parcelable 쓰면 되나?

Serializable 을 구현하고 해당 클래스에 ObjectOutputStream 을 통해 직접 직렬화하고 ObjectInputStream 을 통해 직접 역직렬화 하는 메서드를 선언하여 성능을 개선하는 방법이 있습니다. 그 편이 Parcelable 을 이용하는 것보다 훨씬 빠르다고 합니다. 

 

그도 그럴 것이, Parcelable 을 사용하는 방식 역시 필드를 Parcel 에 작성하고 읽어내는 함수를 필드의 수만큼 호출해야 하기 때문입니다. 반면, Stream 을 통해 직렬화, 역직렬화 하는 방식은 오브젝트를 한 번에 Stream 에 쓰고, 한 번에 읽어내면 됩니다.

 

즉, 가능하면 직접 읽어내고 써내는 메서드를 구현하여 Serializable 을 커스텀하는 편이 좋습니다. 확실히 성능이 좋으니까요. 다만, 이 경우도 예외처리를 포함한 세 개의 메서드를 추가로 작성해야하고, 해당 클래스의 역할이나 책임이 많고 크다면 별도의 직렬화, 역직렬화 메서드 작성으로 인해 가독성과 유지 보수성이 떨어질 수 있으므로 상황에 맞게 취사 선택하여 사용하면 되겠습니다.

 

🤔 Parcelable 도 메서드 오버라이딩하여 작성해야하는데 취사 선택이 웬 말인가요?

@Parcelize
data class AdBanner(
    val createDateTime: String,
    val id: Int,
    val image: String
) : Parcelable

 

코틀린에서는 @Parcelize 어노테이션을 통해, 별도의 보일러 플레이트 코드 없이 Parcelable 을 구현할 수 있습니다. 이 편이 가독성은 훨씬 좋습니다.

 

최초에는 Serializable 이 가독성이 좋고 Parcelable 이 성능이 좋았는데, 이젠 오히려 Serializable 이 성능이 좋고 Parcelable 이 가독성이 좋아졌으니, 재미있는 모양새입니다.