그야말로 별 생각없이 단순작업을 하다 벌어진 일이었습니다.

Java로 작성된 코드들을 일부 Kotlin으로 변환하고 문법을 조금씩 손을 보는 작업을 진행 중이었죠.

컴파일 오류도 없고 빨간 줄 하나 없이 잘 빌드되었고 문제가 없는 줄 알았습니다.

하지만 앱에는 내부적으로 문제가 있었죠. 그 원인은 이런 것이었습니다.

 

Java 클래스 2개가 있었고 일부 동일한 멤버변수들이 가지고 있는 클래스들이었습니다.

이 클래스들을 서로 다른 레이어의 Model로 있으면 레이어 간 데이터 전달시 서로 Mapping되는 역할이 있었죠.

그러다보니 동일한 멤버들을 서로의 멤버에 부어서 변환을 해주는 메서드들이 있었고요.

여기서 apply로 빌더처럼 데이터를 전달하다가 문제가 발생하였습니다.

 

해당코드를 샘플로 다시 작성 해봤습니다.

class Human {
    var sleep: Int = 0
    var eat: Int = 0
    var play: Int = 0
}

class Student {
    var sleep: Int = 0
    var eat: Int = 0
    var study: Int = 0
    
    fun getHuman(): Human {
        return Human().apply {
            sleep = sleep
            eat = eat
        }
    }
}

Human 과 Student 클래스가 있고 Student 클래스를 Human으로 맵핑하면서 동일한 데이터를 전달하는 과정입니다.

아래를 보면 바로 Human 생성 후 return을 하는 것이 보이고 apply로 필요한 데이터를 전달하는 것을 볼 수 있습니다.

하지만 이때 sleep과 eat 변수들을 주의깊게 보질 않았던 것이 문제였습니다.

        return Human().apply {
            sleep = sleep
            eat = eat
        }

위에서 보이는 sleep과 eat는 apply 내에서 this 즉 Human의 멤버들을 뜻하게 됩니다.

그래서 제가 Human에 넘기려던 Student의 데이터들이 제대로 복사가 안되었던 것입니다.

this.sleep = sleep 도 결과는 동일하였습니다.

그래서 아래와 같이 변경하였습니다.

        return Human().also {
            it.sleep = sleep
            it.eat = eat
        }

apply를 also로 변경하고 Human은 it으로 명시적으로 지정을 해주었습니다.

Kotlin의 Scope 함수를 잘 이해하고 조금만 신경 썼으면 틀릴 부분은 아니었습니다.

다만 IDE에서도 문법적으로는 문제가 없어 컴파일 오류로 잡아주질 않아 자칫 잘못하면 헤매기 쉬웠던 문제였죠.

문법들이 편해지는 만큼 방심하기 쉬워지는 것을 다시 한번 느끼긴 했습니다.

 

// 오랫만에 남기는 포스팅이라 뭐라 끝을 맺을지는 모르겠습니다... 그냥 끝!!!

Serializable 과 Parcelable 이란?

Android 에서 작업시 Activity 간 이동이나 다른 앱 Compnent 이동시 Intent를 사용함.

이때 데이터 객체 등을 전달하기 위해 Class를 직렬화하는 부분들을 추가하여 사용함.

Serializable 이나 Parcelable을 사용하게 됨.

보통 개발자 편의성이나 유지보수는 Serializable이 우위고, 런타임시 성능은 Parcelable이 낫다고들 해 옴.

참고로 Serializable에서 직렬화 프로세스를 직접 구현하면 성능까지 더 우위라는 포스팅도 있음.

[https://medium.com/@limgyumin/parcelable-vs-serializable-%EC%A0%95%EB%A7%90-serializable%EC%9D%80-%EB%8A%90%EB%A6%B4%EA%B9%8C-bc2b9a7ba810](https://medium.com/@limgyumin/parcelable-vs-serializable-정말-serializable은-느릴까-bc2b9a7ba810)

위 링크의 포스팅에 잘 설명이 되어 있고 특히 아래 내용은 공감되는 부분이었음.

제 개인적 생각에는 0.000042 밀리초 빠르게 앱을 실행하는것에 집중하기 보다는 차라리 내가 만드는 이 앱 이 사용자가 원하는 일을 잘 처리하고 만족할 만한 결과를 내도록 집중하는것이 더 가치가 있는 일이 아닐까 싶습니다.

Kotlin에서 Parcelable 적용

 

어쨋든 Parcelable을 사용한다고 치고 Kotlin에서 사용시 팁을 정리하겠음.

 

class User {
    val name: String? = null
    val email: String? = null
}

 

위 클래스를 Parcelable을 구현하면 아래와 같음.

import android.os.Parcel
import android.os.Parcelable

class User() : Parcelable {
    var name: String? = null
    var email: String? = null

    constructor(parcel: Parcel) : this() {
        parcel.run {
            name = readString()
            email = readString()
        }
    }

    override fun writeToParcel(dest: Parcel?, flags: Int) {
        dest?.run {
            writeString(this@User.name)
            writeString(this@User.email)
        }
    }

    override fun describeContents(): Int {
        return 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)
        }
    }
}

 

위에서 보다시피 보일러플레이트 코드가 늘어나며, 향후 유지 보수시에도 수정할 부분들이 추가됨.

다행히 Android Extensions 플러그인에서는 실험실 기능으로 아래와 같이 편의기능을 제공함.

1. 실험실 기능 on (app/build.gradle)

android {
    compileSdkVersion 28
    defaultConfig {
		...
    }

    androidExtensions {
        experimental = true
    }

}

 

2. 클래스에 @Parcelize 와 Parcelable 추가 (User.kt)

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelize
class User(val name: String? = null, val email: String? = null) : Parcelable

원래 소스와 달라진 부분은?

프로퍼티들을 주생성자에 추가함.

단순히 @Parcelize 와 Parcelable 을 추가하면 주생성자에 선언된 프로퍼티들만 직렬화 처리를 함.

그렇다면 아래와 같이 프로퍼티를 주생성자에 넣지 않는다면?

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelize
class User() : Parcelable {
    val name: String? = null
    val email: String? = null
}

컴파일 에러는 발생 안함.

다만 런타임시 제대로 프로퍼티들이 직렬화 처리가 안되어 데이터가 없음.

만약 주생성자에서 선언된 것 외의 프로퍼티를 처리해야 한다면 아래와 같이 추가적으로 처리해줘야 함.

import android.os.Parcel
import android.os.Parcelable
import kotlinx.android.parcel.Parceler
import kotlinx.android.parcel.Parcelize

@Parcelize
class User() : Parcelable {
    var name: String? = null
    var email: String? = null

    constructor(parcel: Parcel): this() {
        parcel.run {
            name = readString()
            email = readString()
        }
    }
    private companion object : Parceler<User> {
        override fun User.write(parcel: Parcel, flags: Int) {
            parcel.writeString(name)
            parcel.writeString(email)
        }

        override fun create(parcel: Parcel): User {
            return User(parcel)
        }
    }
}

결국 Android Extensions 플러그인의 기능을 안쓰는 것과 큰 차이는 안나는 듯.

결론

Kotlin에서 Parcelable 사용시 Android Extension 플러그인의 실험실 기능을 사용하면 편해짐.

대신 프로퍼티를 꼭 주생성자에 추가하고 사용하자.

 

  형태 인자 반환
let fun <T, R> T.let(block: (T) -> R): R 호출한 객체 블록 결과값
apply fun T.apply(block: T.() -> Unit): T 호출한 객체 내 메서드 및 속성 객체 자체 반환
run fun <T, R> T.run(block: T.() -> R): R 호출한 객체 내 메서드 및 속성 블록 결과값
with fun <T, R> with(receiver: T, block: T.() -> R): R 호출한 객체 내 메서드 및 속성 블록 결과값

run과 with가 유사하지만 run에서만 Safe Calls를 지원함.

+ Recent posts