[Android] Activity 데이터 전달과 직렬화 - Serializable vs Parcelable

Ju-unn·2026년 8월 26일

Intent가 컴포넌트 사이를 이어주는 메시지라고 정리했다. 이번 글에서는 그 Intent에 실제 데이터를 실어서 Activity끼리 주고받는 방법, 그리고 커스텀 객체를 넘길 때 필요한 직렬화(Serializable/Parcelable)를 다룬다.

Activity끼리 데이터 주고받기

안드로이드에서 하나의 화면(Activity)에서 다른 화면으로 넘어갈 때는 Intent를 사용한다. 이때 단순히 화면만 전환하는 게 아니라, 데이터도 같이 실어 보낼 수 있다. 예를 들어 목록 화면에서 항목을 하나 클릭해서 상세 화면으로 넘어간다고 하면, "어떤 항목을 클릭했는지"에 대한 정보를 같이 넘겨줘야 상세 화면이 뭘 보여줄지 알 수 있다.

Intent(this, DetailActivity::class.java)
    .apply {
        putExtra("movie", dummy[position])
    }

putExtra에 키("movie")와 값(dummy[position])을 넣어서 Intent에 데이터를 실은 다음, 이 Intent로 startActivity를 호출하면 새로 열리는 DetailActivity가 이 데이터에 접근할 수 있다. 받는 쪽에서는 넘겨받은 데이터의 타입에 맞는 get<타입>Extra 메서드를 쓴다.

val movie: Movie = intent?.getParcelableExtra("movie") ?: throw IllegalArgumentException()

여기서 짚고 넘어가야 할 게 있다. 지금 다루는 Intent를 통한 데이터 전달은 서로 다른 두 Activity 사이의 데이터 전달이다. 다음편에서 다룰 onSaveInstanceState는 같은 Activity가 재생성될 때 이전 상태를 이어받는 것이다. 전달 대상도 다르고 시점도 다르다. 나도 처음 배울 때 이 둘을 같은 걸로 뭉뚱그려서 이해했다가 나중에 헷갈렸던 경험이 있어서, 이름이 비슷해 보여도 목적이 다른 개념이라는 걸 먼저 못박고 시작하겠다.

객체를 통째로 넘기려면? - 직렬화가 필요한 이유

위 코드에서 Movie라는 커스텀 객체를 통째로 putExtra에 넣고, 받는 쪽에서는 getParcelableExtra로 꺼냈다. 그런데 왜 그냥 Movie 객체를 변수에 담아 넘기듯이 넘길 수 없고, Parcelable이라는 걸 거쳐야 할까?

Intent는 내부적으로 Bundle이라는 데이터 컨테이너를 사용한다. Bundle은 단순히 메모리 안에서 값을 담아두는 자료구조가 아니라, 프로세스 경계를 넘나들 수 있는 Binder IPC(Inter-Process Communication, 프로세스 간 통신) 구조를 전제로 설계되어 있다. Activity를 시작할 때 실제로는 안드로이드 시스템(정확히는 ActivityManagerService)이 중간에 개입해서 새 Activity를 띄우는데, 이 과정이 프로세스 경계를 넘는 경우가 있다. 그래서 Bundle에 담기는 데이터는 "메모리 주소를 그대로 복사"하는 방식으로는 전달할 수 없고, 바이트 단위로 직렬화(serialize, 객체를 전송·저장 가능한 형태로 변환하는 것)해서 보낸 다음 반대편에서 역직렬화(deserialize)해서 복원하는 절차를 거쳐야 한다.

일반 자바/코틀린 객체는 이런 직렬화 규칙이 없다. 그래서 안드로이드는 "이 객체는 직렬화가 가능하다"는 걸 명시적으로 보장하는 인터페이스를 요구한다. 그게 바로 Serializable과 Parcelable이다. 둘 중 하나를 구현해야만 Bundle(따라서 Intent)에 커스텀 객체를 담아 넘길 수 있다.

Serializable vs Parcelable

Serializable과 Parcelable은 둘 다 "이 객체는 직렬화할 수 있다"는 걸 표시하는 인터페이스라는 점에서 기능은 같다. 하지만 구현 방식과 성능에서 차이가 크다.

구분SerializableParcelable
구현 방법인터페이스만 구현하면 끝인터페이스 구현 + 추가 코드 작성 필요
성능상대적으로 느림상대적으로 빠름
코드 작성량추가 코드 작성 불필요writeToParcel(), describeContents(), CREATOR 등 보일러플레이트 필요
의존성Java 표준 API안드로이드 의존성 필요

표만 외우면 "그냥 Serializable이 편해 보이는데 왜 Parcelable을 쓰라는 거지"라는 생각이 들 수 있다. 그래서 성능 차이가 왜 나는지 원리를 짚어보겠다.

Serializable은 리플렉션(reflection, 런타임에 클래스의 필드나 구조를 분석하는 기법)을 이용해서 객체를 분석하고 직렬화한다. 클래스에 어떤 필드가 있는지, 타입이 뭔지를 실행 중에 하나하나 들여다보면서 처리하기 때문에 느리고, 이 과정에서 임시 객체가 많이 만들어져서 가비지 컬렉션(GC, 더 이상 쓰지 않는 메모리를 회수하는 작업) 부담도 커진다. 반면 Parcelable은 리플렉션을 쓰지 않는다. 클래스를 만들 때 "이 필드를 이 순서로 읽고 쓴다"는 로직을 직접 정의해두기 때문에, 실행 시점에 구조를 분석할 필요 없이 메모리에 필드를 바로 읽고 쓴다. 그만큼 빠르다.

Serializable은 원래 자바 표준 API로, 안드로이드 전용으로 최적화된 게 아니다. 그런데 안드로이드는 Activity 간, 프로세스 간 데이터 전달이 매우 빈번한 플랫폼이라서, 이 경로에 최적화된 자체 인터페이스로 Parcelable을 따로 만들어 제공한다. 그래서 안드로이드 공식 문서도 IPC 상황에서는 Parcelable 사용을 권장한다.

다만 표에서 본 것처럼 Parcelable은 직접 구현하면 writeToParcel(), describeContents(), CREATOR 필드까지 보일러플레이트 코드를 다 작성해야 한다는 단점이 있다. 이 단점을 없애주는 게 다음에 볼 @Parcelize 플러그인이다.

Parcelize로 보일러플레이트 없애기

Parcelable을 직접 구현하면 코드가 이렇게 길어진다.

// Before: Parcelable 직접 구현
class Movie(val title: String, val year: Int) : Parcelable {
    constructor(parcel: Parcel) : this(
        parcel.readString() ?: "",
        parcel.readInt()
    )

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(title)
        parcel.writeInt(year)
    }

    override fun describeContents(): Int = 0

    companion object CREATOR : Parcelable.Creator<Movie> {
        override fun createFromParcel(parcel: Parcel): Movie = Movie(parcel)
        override fun newArray(size: Int): Array<Movie?> = arrayOfNulls(size)
    }
}

필드가 두 개뿐인 클래스인데도 이만큼 써야 한다. 필드가 늘어날수록 이 보일러플레이트도 같이 늘어난다. 코틀린은 이 문제를 kotlin-parcelize라는 공식 플러그인으로 해결한다. build.gradle.kts에 플러그인을 추가한다.

plugins {
    id("kotlin-parcelize")
}

그리고 클래스에 @Parcelize 어노테이션을 붙이고 Parcelable을 상속하기만 하면, writeToParcel()이나 CREATOR 같은 코드를 컴파일 시점에 자동으로 만들어준다.

// After: @Parcelize 적용
import kotlinx.parcelize.Parcelize
import android.os.Parcelable

@Parcelize
data class Movie(val title: String, val year: Int) : Parcelable

코드 한 줄로 줄었다. 여기서 주의할 점은, 직렬화 대상 프로퍼티는 반드시 주 생성자(primary constructor)에 선언되어야 한다는 것이다. 클래스 본문에 따로 선언한 프로퍼티는 자동으로 직렬화되지 않고 경고가 뜬다. 원시 타입, String, CharSequence, List/Set/Map 같은 컬렉션, Bundle, 그리고 이미 Serializable이나 Parcelable을 구현한 타입이라면 nullable 버전이나 배열까지도 기본으로 지원한다. 이 기본 지원 범위를 벗어나는 커스텀 타입을 다뤄야 한다면 Parceler 인터페이스를 직접 구현해서 @TypeParceler로 연결하는 방법도 있는데, 이건 심화 내용이라 이런 게 있다는 정도만 알아두면 된다.

헷갈리는 포인트

Intent로 데이터 넘기는 것과 onSaveInstanceState로 상태 저장하는 것을 같은 것으로 착각. 앞서 못박았듯, 전자는 서로 다른 두 Activity 사이의 데이터 전달이고 후자는 같은 Activity가 재생성될 때 이전 상태를 이어받는 것이다. 목적도 시점도 다르다.

Serializable과 Parcelable 이름이 비슷해서 아무거나 골라도 되는 줄 아는 착각. 둘 다 직렬화를 위한 인터페이스라는 점은 같지만, 리플렉션 사용 여부 때문에 성능 차이가 있고, 안드로이드 공식 권장은 Parcelable, 그중에서도 @Parcelize다.

마무리 정리

이번 글에서는 서로 다른 두 Activity 사이에서 Intent로 데이터를 주고받는 방법과, 커스텀 객체를 넘길 때 필요한 Serializable/Parcelable 직렬화를 정리했다. Parcelable이 리플렉션 없이 직접 정의한 읽기/쓰기 로직으로 동작해서 더 빠르고, @Parcelize를 쓰면 그 보일러플레이트도 거의 없앨 수 있다는 게 핵심이었다.

profile
한줄한줄 기록해보자

0개의 댓글