본문 바로가기
안드로이드/Android

[Android] 직렬화(Serialization, Parcelable), 역직렬화(Deserialization)

by jinwo_o 2024. 10. 27.

Serialization(직렬화)

  • 객체를 외부의 시스템에서도 사용할 수 있도록 객체의 데이터를 *바이트 스트림(연속적인 바이트) 형태로 변환하는 기술
    • 바이트 스트림 : 스트림은 클라이언트나 서버 간에 출발지 목적지로 입출력하기 위한 데이터가 흐르는 통로를 말한다. 자바는 스트림의 기본 단위를 바이트로 두고 있기 때문에, 네트워크나 데이터베이스로 전송하기 위해 최소 단위인 바이트 스트림으로 변환하여 처리한다.
  • JVM 의 메모리(힙 or 스택)에 상주되어 있는 객체 데이터를 바이트 형태로 변환하는 기술
  • 바이트 형태로 변환 후, 데이터베이스나 파일과 같은 외부 저장소에 저장해두고, 다른 컴퓨터에서 이 파일을 가져와 역직렬화를 통해 객체로 변환하여 JVM 메모리에 적재하는 것
  • 직렬화를 응용한다면 휘발성이 있는 캐싱 데이터를 영구 저장이 필요할 때 사용할 수도 있다.
    • 데이터베이스로부터 조회한 객체 데이터를 다른 모듈에서도 필요할 때 재차 DB 를 조회하는 것이 아닌, 객체를 직렬화하여 메모리나 외부 파일에 저장해 두었다가 역직렬화하여 사용하는 캐시 데이터로서 이용이 가능하다.

 

Serializable

  • Android 의존성을 갖지 않는 표준 Java 의 인터페이스
  • 별도로 구현해야 할 메서드 없이, 사용할 곳에서 java.io.Serializable 을 구현하면 된다. (Marker Interface)
  • String, Array, Enum, Serializable 이외 타입에 대해서 예외를 던진다.
  • ObjectOutputStream 객체의 writeObject() 메서드를 통해 직렬화를 수행하고, writeObject 는 ObjectOutputStream 의 writeObject0 를 실행한다.
import java.io.*

fun main() {
    val person = Person("GildongHong", 30)
    
    val byteStream = serialize(person)
}

@Throws(IOException::class)
fun serialize(o: Any): ByteArray {
    ByteArrayOutputStream().use { baos ->
        ObjectOutputStream(baos).use { oos ->
            oos.writeObject(o)
            return baos.toByteArray()
        }
    }
}

data class Person(val name: String, val age: Int) : Serializable

 

 

Deserialization(역직렬화)

  • 바이트로 변환된 데이터를 다시 객체로 변환하는 기술
  • 직렬화된 바이트 형태의 데이터를 객체로 변환해서 JVM 으로 상주시키는 과정
  • 직렬화된 byte-stream 은 ObjectInputStream 의 readObject 를 통해 객체를 얻은 후, 타입 캐스팅을 통해 원본 객체로 역직렬화하게 된다.
  • 해당 객체는 반드시 동일한 *serialVersionUID 를 가져야 한다.
    • serialVersionUID(SUID) : Serializable 인터페이스를 구현하는 모든 객체가 부여 받는 고유의 식별번호
      • 이 식별번호를 통해 직렬화, 역직렬화 시 동일한 객체인지를 확인하게 된다. 따라서, 객체를 구성하는 멤버들의 구성이 수정될 경우 이 식별번호가 달라지기 때문에 예외 (InvalidClassException)가 발생되게 된다.
      • 객체의 식별번호를 명시하지 않아도, 런타임 중에 JVM이 자동으로 별도의 해시 함수를 통해 식별번호를 생성한다. 하지만 안전한 직렬화와 역직렬화를 위해서는 명시하는 것이 좋다.
import java.io.*

fun main() {
    val person = Person("JaesungLeee", 30)
    
    val byteStream = serialize(person)
    
    val newPerson = deserialize(byteStream) as Person
    println("Deserialized Person: $newPerson")
}

@Throws(IOException::class)
fun serialize(o: Any): ByteArray {
    ...
}

@Throws(IOException::class, ClassNotFoundException::class)
fun deserialize(bytes: ByteArray): Any {
    ByteArrayInputStream(bytes).use { bais ->
        ObjectInputStream(bais).use { ois ->
            return ois.readObject()
        }
    }
}

data class Person(val name: String, val age: Int) : Serializable

 

Serializable 의 단점

  • 안전하지 못한 SUID : SUID 를 수동으로 직접 명시한다 하더라도 완벽하다고는 볼 수 없다. 직렬화할 객체에 새로운 필드가 추가되는 것이 아닌 타입이 변경될 경우에도 예외 (InvalidClassException)는 발생하게 된다.
  • 역직렬화 시 발생하는 과도한 *Reflection : 역직렬화 할 객체에 대해 ObjectInputStream 의 readOrdinaryObject() 메서드를 수행하면, 내부적으로 Reflection 을 통해 불필요한 객체들이 생성되게 된다. 이러한 불필요한 객체들은 과도한 GC 의 발생 원인이 될 수 있기 때문에 성능적으로 좋지 않다.
    • Reflection :  구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API 로, 컴파일 시간이 아닌 실행 시간에 동적으로 특정 클래스의 정보를 추출할 수 있는 프로그래밍 기법이다.
    • Reflection 에 의한 성능 저하는 자동 직렬화/역직렬화 프로세스에 의한 현상이기 때문에 직접 writeObject 와 readObject 를 구현하여 개선할 수 있다.
@Throws(IOException::class)
private fun writeObject(out: ObjectOutputStream) {
    out.apply {
        writeUTF(name)
        writeInt(age)
    }
}

@Throws(IOException::class, ClassCastException::class)
private fun readObject(`in`: ObjectInputStream) {
    `in`.run {
        name = readUTF()
        age = readInt()
    }
}

Android Parcelable

  • Serializable 과 마찬가지로 직렬화에 쓰일 수 있는 인터페이스
  • 순수 언어 의존성을 갖지 않고 Android 의존성을 갖고 있다.
  • Serializable 과 달리, 직렬화 처리 방법이 자동으로 처리되지 않으며, 개발자가 직접 명시적으로 작성할 수 있도록 메서드를 제공한다. (Reflection 도 존재하지 않다)
  • 직렬화는 Parcelable 의 *writeToParcel() 메서드를 통해 이루어진다.
    • writeToParcel() : 직렬화 시 contrainer 역할을 하는 Parcel 안에 데이터를 넣는 작업을 한다.
  • Serializable 은 ObjectInputStream 을 통해 직렬화 할 데이터를 작성했다면, Parcelable 은 Parcel 이라는 객체를 통해 직렬화 할 데이터를 작성한다.
class Person() : Parcelable {
    var name: String? = null
    var age: Int? = null
  
    ...
  
    override fun writeToParcel(dest: Parcel?, flags: Int) {
        dest?.run {
          writeString(this@Person.name)
          writeInt(this@Person.name)
      }
    }
  
    ...
}

 

  • describeContents() : Parcel 객체에 내용을 기술한다. FileDescriptor 같은 특별한 객체가 들어가면 해당 메서드를 통해 알려줘야 한다. 보통은 0을 리턴한다.
  • 역직렬화는 Parcelable 의 하위 인터페이스인 Creator 의 *createFromParcel() 메서드를 통해 수행할 수 있다.
    • Creator 라는 static field(정적 필드)를 반드시 가지고 있어야 한다. 이는 선언과 동시에 초기화 되어야 한다.
    • createFromParcel() : parcel 된 데이터를 다시 원래 객체로 변환시켜 주는 작업을 한다.
    • newArray() : 전달하는 객체가 배열일 경우 즉, Parcel.createTypeArray() 를 호출했을 때 불리며 배열을 다시 할당하기 위해 사용한다.
class Person() : Parcelable {
    var name: String? = null
    var age: Int? = null
  
    ...
    
    override fun describeContents(): Int {
        return 0
    }
  
    companion object CREATOR : Parcelable.Creator<Person> {
        override fun createFromParcel(parcel: Parcel): Person {
            return Person(parcel)
        }

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

 

@Parcelize Annotation

  • Android 의존성을 갖는 Parcelable 을 사용하여 직렬화를 개발자가 직접 구현할 수 있다는 장점이 있지만, 많은 보일러-플레이트를 만들게 된다는 단점도 존재한다. 이러한 단점은 가독성을 해치기도 할 뿐만 아니라 새로운 기능의 확장이 어려워진다. 이를 위해, 안드로이드에서는 @Parcelize Annotation 을 제공하고 있다.
  • kotlin-parcelize 플러그인은 Parcelable 의 구현을 자동으로 해주기 때문에, Annotation 의 추가 만으로도 직접 Parcelable 관련 코드를 오버라이드하지 않고도 동일하게 동작한다.
  • 조금 더 복잡한 직렬화 로직이 필요할 경우 선택적으로 구현할 수 있다.

Parcelable vs Serializable

  • Serializable 은 자동 직렬화 메커니즘을 사용하기 때문에 Reflection 이라는 단점이 존재하지만, 개발자가 직접 직렬화 로직을 정의할 수 있는 Parcealable 과 비교했을 때 무조건 더 느리다고 단정할 수는 없다.
    • Parcelable 이 Serializable 보다 빠른 이유 : Parcelable 은 프로세스 간의 데이터 전송을 위한 IPC(프로세스 간 통신) 기법을 이용하기 때문이다. 프로세스 간의 메모리 영역은 서로 공유할 수 없기 때문에, Parcelable 인터페이스는 커널 메모리를 통해 데이터를 다른 프로세스로 전달하는 통로를 만들어준다.
  • 레이어 간의 의존성을 완전히 분리하고, 이에 따라 도메인 모델과 UI 모델을 구분하게 되면 이를 어떻게 활용할지 고민해볼 수 있다.
    • 도메인 모델에 직렬화가 필요하다면, 안드로이드 의존성을 가진 Parcelable 보다는 Serializable 사용이 더 적절하다.
    • 화면 간 객체 전달을 위해 직렬화를 사용한다면, 도메인 모델에 직렬화의 책임을 주는 것보다 UI 모델에 직렬화의 책임을 주는 것이 적절해 보인다. 따라서 UI 모델에서는 Paracelable 을 사용하는 것이 좋을 것 같다.
  • 또한, 화면 간 객체 전달을 위해 Intent 를 사용할 때 Parcelable 은 기본 메서드를 통해 객체 전달을 지원하고 있다. 반면, Serializable 은 이러한 리스트 형태로 객체를 전달하는 데 어려움이 있다. 편의성을 생각해보면 Presentation Layer 에서 Parcelable 사용이 더 적절해 보인다.
    • Intent.putExtra(String, Parcelable)
    • Intent.putParcelableArrayListExtra(String, ArrayList<out Parcelable>)

 

(Android) 직렬화와 역직렬화

Serializable, Parcelable 선택 기준

medium.com

 

[Android] 직렬화

직렬화란? 역직렬화란? Serializable Parcelable

woovictory.github.io

 

☕ 자바 직렬화(Serializable) - 완벽 마스터하기

자바의 직렬화 & 역직렬화 직렬화(serialize)란 자바 언어에서 사용되는 Object 또는 Data를 다른 컴퓨터의 자바 시스템에서도 사용 할수 있도록 바이트 스트림(stream of bytes) 형태로 연속전인(serial) 데

inpa.tistory.com

'안드로이드 > Android' 카테고리의 다른 글

[Android] SharedPreference vs Datastore  (0) 2024.11.05
[Android] Strong, Soft, Weak, Phantom Reference  (0) 2024.10.21
[Android] View  (0) 2024.10.05