Uing? Uing!!

[내 맘대로 정리한 Kotlin] @JvmOverloads: constructor를 일일이 상속받아 만들기 귀찮다면! 본문

Kotlin

[내 맘대로 정리한 Kotlin] @JvmOverloads: constructor를 일일이 상속받아 만들기 귀찮다면!

Uing!! 2021. 3. 30. 05:04
반응형

생성자(constructor)

클래스의 생성자를 설정하는 데에는 다양한 방법이 있다.

아래처럼 주 생성자를 설정하는 방법이 가장 대표적이라고 할 수 있겠다.

class Dog(val name: String, val age: Int)

하지만 여러 종류의 생성자가 필요한 경우도 있다.

이를테면 name, age 중 일부만 가지고 생성하고 싶다면 이렇게 쓸 수 있을 것이다.

class Person {
    private var name: String = ""
    private var age: Int = 0
    
    constructor()
    constructor(name: String) {
        this.name = name
    }
    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

fun main() {
    val person1 = Person()
    val person2 = Person("Sam")
    val person3 = Person("Sam", 30)
}

물론 코틀린에서 위의 기능을 위해서 이렇게 복잡한 생성자를 사용해야 하지는 않는다.

위 코드는 default값을 적용하여 이렇게 축약이 가능하다.

class Person(
    private val name: String = "", 
    private val age: Int = 0
) 

fun main() {
    val person1 = Person()
    val person2 = Person("Sam")
    val person3 = Person("Sam", 30)
}

 

생성자 오버로딩(Constructor Overloading)

하지만 코틀린을 사용하더라도 constructor를 작성해주어야 하는 경우가 있다.

이를테면 View를 상속받은 클래스를 생성할 때에는, 아래 3개의 생성자가 필요하다.

View(context: Context)
View(context: Context, attrs: AttributeSet)
View(context: Context, attrs: AttributeSet, defStyleRes: Int)

이 중 하나만 상속받아 아래처럼 작성할 수도 있어 보이지만...

class CustomView(
    context: Context, 
    attrs: AttributeSet? = null, 
    @AttrRes defStyleAttr: Int = 0
) : View (context, attrs, defStyleAttr)

실제로 xml을 뷰를 사용해 보면 예상치 못한 크래시가 난다.

내부적으로 xml을 inflate해주는 과정에서 위의 3개 constructor가 실행되기 때문이다.

 

여러 상황에서 정상적으로 작동하는 코드를 작성하려면 각각의 constructor가 필요하다.

위 CustomView를 constructor를 이용해 작성한다면 이런 코드가 된다.

class CustomView: View {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int) : super(context, attrs, defStyleAttr)
}

하지만 이런 cosntructor는 보일러플레이트 코드가 된다.

매번 커스텀뷰, 커스텀레이아웃을 만들 때마다 constructor를 겹겹이 써야 하는 것은 생각보다 번거로운 일이다.

 

@JvmOverloads 어노테이션

@JvmOverloads는 위와 같은 생성자 오버로딩을 자동으로 생성해 주는 어노테이션이다.

이 어노테이션을 사용하면 위와 같은 보일러플레이트 코드를 최소화할 수 있다.

 

위의 Student 클래스를 @JvmOverloads를 사용하여 작성하면 이렇다.

class CustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    @AttrRes defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes)

이를 디컴파일해 보면 실제로 아래와 같은 생성자들이 모두 생성된다.

public CustomView(@NotNull Context context) 
public CustomView(@NotNull Context context, @Nullable AttributeSet attrs) 
public CustomView(@NotNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) 
public CustomView(Context var1, AttributeSet var2, int var3, int var4, DefaultConstructorMarker var5) 

 

비교 분석

처음에 @JvmOverloads 없이 시도했던 버전과, 모든 constructor를 직접 작성한 버전, @JvmOverloads가 사용된 버전을 각각 비교해 보자.

@JvmOverloads 없이 시도했던 버전

class CustomView(
    context: Context, 
    attrs: AttributeSet? = null, 
    @AttrRes defStyleAttr: Int = 0
) : View (context, attrs, defStyleAttr)

이 코드는 디컴파일 시에 아래 두 생성자가 만들어진다.

public CustomView(@NotNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) 
public CustomView(Context var1, AttributeSet var2, int var3, int var4, DefaultConstructorMarker var5) 

Kotlin 코드에서 CustomView(context)의 형식으로 뷰를 생성하면 두 번째 생성자가 적용되지만,

내부적으로 xml 파일을 inflation할 경우 이런 에러가 발생한다.

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.kakao.keditor.example, PID: 18189
    android.view.InflateException: Binary XML file line #24 in com.kakao.keditor.example:layout/ke_lego_item_emoticon: Binary XML file line #24 in com.kakao.keditor.example:layout/ke_lego_item_emoticon: Error inflating class com.kakao.kemoticon.EmoticonView
    Caused by: android.view.InflateException: Binary XML file line #24 in com.kakao.keditor.example:layout/ke_lego_item_emoticon: Error inflating class com.kakao.kemoticon.EmoticonView
    Caused by: java.lang.NoSuchMethodException: com.kakao.kemoticon.EmoticonView.<init> [class android.content.Context, interface android.util.AttributeSet]
        at java.lang.Class.getConstructor0(Class.java:2332)
        .
        .
        .

에러 메시지를 보면, xml을 inflate하는 과정에서 getConstructor0을 호출하는데,

이 호출에서 NoSuchMethodException이 발생한다.

메시지에서 View(context: Context, attrs: AttributeSet) 생성자가 없어서 발생한 Exception임을 알 수 있다.

constructor를 직접 작성한 버전

class CustomView: View {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int) : super(context, attrs, defStyleAttr)
}

이 코드는 디컴파일되어 이런 생성자들을 만든다.

public CustomView(@NotNull Context context) 
public CustomView(@NotNull Context context, @Nullable AttributeSet attrs) 
public CustomView(@NotNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) 
public CustomView(Context var1, AttributeSet var2, int var3, DefaultConstructorMarker var4) 
public CustomView(Context var1, AttributeSet var2, int var3, int var4, DefaultConstructorMarker var5) 

 

이중 위쪽 3개 생성자는 안드로이드 내부에서 호출할 때 찾아서 사용되고,

아래 2개는 코드 상에서 직접 CustomView(context) 등을 사용할 때 변환되어 호출되는 것으로 보인다.

(정확하게 어떤 시점에 각 생성자가 사용되는지는 확인해보지 않았다.)

@JvmOverloads가 적용된 버전

class CustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    @AttrRes defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes)

앞서 언급했듯이 이 코드는 이런 생성자들을 만들어낸다.

public CustomView(@NotNull Context context) 
public CustomView(@NotNull Context context, @Nullable AttributeSet attrs) 
public CustomView(@NotNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) 
public CustomView(Context var1, AttributeSet var2, int var3, int var4, DefaultConstructorMarker var5) 

constructor를 모두 작성하는 생성 방식에 비해 var1, var2...등이 포함된 생성자가 적게 만들어진다.

하지만 마지막 생성자가 모든 케이스를 커버할 수 있기 때문에 동작 상의 문제는 없는 듯하다.

결론

@JvmOverloads 어노테이션을 사용하면 여러 개의 constructor를 상속받는 번거로움이 줄어들고,

xml inflation 시에도 문제가 발생하지 않는다.

 

다만 @JvmOverloads를 사용할 때에는 attrs, defStyleAttr에서와 같이 default 값을 사용하게 되는데,

실제 전개된 코드를 보면 해당 파라미터값이 누락될 경우 default값이 적용된다.

따라서 상속받은 코드에서 default가 아닌 값이 사용되는 경우에는 예상과 다른 작동이 있을 수 있다.

 

하지만 단순하게 View 또는 FrameLayout 정도를 상속받아 사용하는 데 있어서만큼은 정말 편리하고 유용한 도구이다.

또한 위 예외에 해당하는 클래스를 상속받게 되더라도,

사용 전에 상속받을 대상의 constructor를 쭉 한번 훑어보고 사용한다면 문제 없이 사용할 수 있을 것이다.

(이러한 케이스에 대해서는 추후 다른 포스팅에서 다룰 수도 있다.)

 

 

 

TMI) 안드로이드, 코틀린 중 어느 분야에 쓸지 고민했다.

 

반응형
Comments