쾌락코딩

람다 내부의 return은 언제 불가능하며 이유는 무엇일까?

|

고차함수에 넘긴 람다에서 return은 사용 가능할 때가 있고, 불가능 할 때가 있다. 정확하게는 인라인 고차함수의 경우 return 사용이 가능하고, 일반 고차함수는 return 사용이 불가하다.

예를 들어 흔히 사용하는 forEach의 경우 inline 고차함수라서 람다 내부에서 return이 가능하다.


fun findAndPrintTen(numbers: List<Int>) {
    numbers.forEach {
        if (it == 10) {
            println("find 10!!")
            return
        }
    }
}

fun main() {
    findAndPrintTen(listOf(1,2,3,10))
}

// find 10!!
// inline 함수로 구현된 forEach 내부 
@kotlin.internal.HidesMembers
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

그러나 inline이 아닌 일반 고차함수의 lambda에서는 return을 사용 할 수 없다. 원인을 알아보기 forEach를 직접 만들어 보며 테스트 해보자.

inline fun <Int> Collection<Int>.forEachA(block: (Int) -> Unit) {
    for (e in this) block(e)
}

fun <Int> Collection<Int>.forEachB(block: (Int) -> Unit) {
    for (e in this) block(e)
}

forEachAinline 고차함수고, forEachB는 일반 고차함수다. forEachA로 넘긴 람다에서는 return을 사용할 수 있고, forEachB로 넘긴 람다에서는 return을 사용하지 못할 것이다. 확인을 위해 findAndPrintTen() 함수로 돌아가서 forEachAforEachB를 사용하도록 한 번씩 변경해보자.

fun findAndPrintTen(numbers: List<Int>) {
    numbers.forEachA {
        if (it == 10) {
            println("find 10!!")
            return
        }
    }
}

fun findAndPrintTen(numbers: List<Int>) {
    numbers.forEachB {
        if (it == 10) {
            println("find 10!!")
            return  // compile error : 'return' is not allowed here
        }
    }
}

언급했던 대로 forEachB는 컴파일되지 못한다. 해당 위치에서는 ‘return’을 사용하지 못한다는 에러를 내뿜는다.

왜이럴까?

forEachA의 경우는 inline 함수이기 때문에 파라미터로 받는 람다가 호출하는 쪽에 inline된다. 예를 들어, 컴파일러는 아래 코드를

fun findAndPrintTen(numbers: List<Int>) {
    numbers.forEachA {
        if (it == 10) {
            println("find 10!!")
            return // findAndPrintTen를 return 시킨다
        } 
    }
}

아래처럼 변환시킨다.

fun findAndPrintTen(numbers: List<Int>) {
    for (number in numbers) {
        if (number == 10) {
            println("find 10!!")
            return // findAndPrintTen를 return 시킨다
        }
    }
}

forEachA로 넘긴 람다 block이 컴파일러에 의해 그대로 inline되기 때문에 return문을 만나면 일반 for문에서 return을 하는 것과 동일하게 findAndPrintTen를 리턴시킬 수 있다. 그래서 inline 고차함수로 넘긴 람다에서는 return이 허용된다.

반면 forEachB 파라미터로 받은 람다의 경우 호출하는 쪽에 inline되지 않는다. 람다로 받은 함수는 컴파일러에 의해 아무런 처리가 되지 않고, 컴파일 된 이후 Function  타입의 객체가 된다.

람다가 결국 Function 타입의 객체라면, 이 람다를 변수에 저장할 수도 있다는 뜻인데 이런 현상 때문에 return이 금지되는 것이다(inline 함수의 람다는 객체가 아니므로 변수에 저장 불가). 만약 람다를 외부에 저장해놓고, 자신을 호출한 함수의 context를 벗어난 곳에서 함수가 실행 된다면 예상치 못한 버그가 일어날 수 있기 때문이다. 위 코드의 경우에는 findAndPrintTen 함수가 이미 끝난 상태에서 어딘가에 저장된 람다가 다시 호출 되는 경우라고 볼 수 있다. 이미 늦은 시점이다.

실제로 아래의 코드를 보면, inline 고차함수로 넘긴 람다는 다른 변수에 저장이 불가능 하다(그래서 return이 허용). 반면 일반 고차함수로 넘긴 람다는 다른 변수로 저장이 가능하여 return문으로 인한 버그 방지를 위해 return을 금지한다.

var savedFunc: () -> Unit = {}

inline fun test1(block: () -> Unit) {
    savedFunc = block // compile error:  inline될 것이기 때문에 변수로 저장할 수 없다.
}

fun test2(block: () -> Unit) {
    savedFunc = block
}

Jetpack Compose가 필요한 이유(Mental Model)

|

요즘 kotlin weeklyandroid weekly를 보면, Jetpack Compose 이야기가 굉장히 핫합니다. 몇 년전, 매주 Weekly나 Medium 또는 Google 영상을 찾아보면 거의 절반 정도가 coroutine이야기였을 때가 있었는데요, 그때와 굉장히 비슷하다고 생각합니다. 많은 사람들이 coroutine에 관심을 많이 가졌고, 좋은 글들이 많이 올라왔습니다. 특히나 Rx와 Coroutine을 비교하는 글들이 많았던 것 같고, 2021년 현재 상당히 많은 회사에서 Rx를 걷어내고 Coroutine을 적용하고 있습니다.

시대를 좀 더 거슬러 올라가면 Java가 아닌 Kotlin을 써야 하는 이유, Kotlin의 장점 등에 대한 글이 굉장히 많이 나왔습니다. 결국 그 기술들이 서서히 자리 잡게 되었습니다.

최근 몇 달간의 아티클들을 보았을 때 어떤 때는 절반 넘게 Jetpack Compose 이야기일 때도 있습니다. 개인적으로 굉장히 기다렸던 기술이라서 직접 사용해 보기도 하고, 여러 아티클들을 많이 읽고 있는데요, 공부했던 부분들을 공유해보려고 합니다.

이 글은 Compose를 다루는 방법에 대한 이야기는 아닙니다. 그보다는 왜 Compose가 나왔고, 왜 Compose가 피할수 없는 미래인지에 대한 이야기입니다. 2021년 하반기에 정식 릴리즈가 되면, 그저 정식 릴리즈 되었으니 쓰는게 아니라, 왜 컴포즈가 나오게 되었는지를 알고 써야 컴포즈스럽게 쓸 수 있다고 생각합니다.

컴포즈가 필요한 이유는 크게 아래의 세가지 이유로 정리할 수 있습니다.

  1. xml을 벗어난 UI 개발
  2. 선언형 UI
  3. 상속이 아닌 확장

1. XML을 벗어난 UI 개발

image

몇 년 동안 코드와 UI를 연결하는 많은 방법들이 나왔습니다. 그림에서 보이듯이 계속해서 꾸준히 발전 해왔고, 지금 현재 MVVM과 databinding 조합으로 꾀나 잘 사용하고 있는데, 왜 컴포즈가 나왔을까요? 이들의 한계는 결국 여전히 xml을 사용한다는 점에 있습니다.

image

위 그림은 Xml으로 UI를 그릴 경우에 흔히 나타나는 패턴을 나타냅니다. view와 데이터 사이에 중간 계층이 하나 끼어들어야만 하는 형태입니다.

UI를 그리기 위해 XML을 사용하고 있음에도 불구하고, 코틀린 역시 UI에 어떤 id 값이 있고, 어떤 UI를 포함하려 하는지를 알고 있어야 하기 때문에 둘은 강하게 의존하게 됩니다. 시간이 많이 흐른 뒤 view를 수정하고 싶어서 xml을 수정하게 되면, 어쩔 수 없이 kotlin 코드도 수정해야 하며, 이때 하나라도 놓치면 런타임 에러가 나게 됩니다. 결국 코틀린 개발자와 코틀린 코드가 어떠한 안전장치도 없이 UI에 개념적으로 의존해야 한다는 의미이기도 합니다.

MVVM과 databinding 조합은 그나마 상황이 낫습니다. bindingAdapter를 사용하면 programming language에서 코드적으로나 개념적으로 UI의 구조에 덜 의존하게 될 수 있게 되고, 어느 정도는 동적인 UI 제어가 가능해지긴 했습니다. 그럼에도 불구하고 여전히 XML을 벗어나지 못했다는 한계가 있는데요, 특정 view의 attritue 정도를 동적으로 적용하기 쉬울 뿐, UI 그룹들을 동적으로 바꾸기에는 적합하지 않습니다. Fragment를 써서 Kotlin 코드로 Fragment를 교체하는 게 그나마 나은 방법이 되겠지만, Fragment 역시 쉽지 않습니다. code로 Fragment를 조작하는 일은 매우 귀찮은 작업일 뿐만 아니라 생명 주기도 까다로워서 예상치 못한 상황에 애를 먹기도 합니다.

Jetpack Compose를 사용하면 Fragment는 자연스럽게 없어집니다. 아래에서 다시 다루도록 하겠습니다.

image

위 그림은, 선언형 UI toolkit인 Jetpack Compose(only kotlin)로 구현할 경우 나타나는 패턴입니다. 즉, 데이터를 입력받으면 UI를 그려주는 Kotlin 함수(composable 함수)를 사용하여 UI를 그리게 되는 상황인데요, ViewModel에서 변경된 데이터를 Composable 함수의 파라미터로 바로 넘겨주기만 하면 되기 때문에, 서로 다른 분야를 이어주며 강한 결합도를 가지는 Extra Layer가 사라지게 됩니다. UI가 바뀌면, Composable 함수만 수정하면 될 뿐입니다.

Goodbye Fragment

Fragment의 기원은 Android 3.0에서 시작합니다. 태블릿 PC처럼 Activity 한 개에 보일 화면(View)들이 많을 경우 Activity Class 하나에 코드가 넘쳐나는 것을 방지하고자 세상에 나왔습니다. fragment 액티비티 한 개만 사용하는 상황에서, 두 화면을 각자 독립적인 클래스로 다룰 때도 결국은 각각 Activity의 Lifecycle이 필요합니다. 즉 Fragment는 Android 컴포넌트가 아니라 단지 Activity의 생명주기를 추종하는 View라고 볼 수 있습니다. 따라서 하나의 Activity에 성격이 다른 화면(View)이 두 개 필요할 때, Fragment를 사용하면 findViewById와 기타 로직이 Activity 하나에 너무 많아지는 걸 방지할 수 있게 되었습니다. 그러나 결국 Lifecycle을 추종하는 View 일 뿐이기 때문에 UI Toolkit이 Jetpack Compose로 바뀌면 View를 위한 Fragment의 사용성은 사라집니다. findViewById는 역사속으로 사라지고, 기타 로직은 ViewModel에서 수행하기 때문에 위의 그림 처럼 Fragment가 설 자리는 없어집니다.

실제로 React나 Flutter같은 선언형 UI 들을 보면 fragment같은 개념이 없습니다.

2. 선언형 UI

명령형 프로그래밍과 선언형 프로그래밍이란 단어를 한 번쯤 들어 보았을 것 같습니다. 명령형 프로그래밍은 우리가 흔히 코딩하듯이, 컴퓨터에게 하나하나 명령을 하는 형태로 코딩을 하는 것입니다. 반면에 선언형 프로그래밍은 컴퓨터에게 우리가 원하는 것을 선언 또는 표현하듯이 코딩 하는 것입니다.

명령형 프로그래밍

명령형 코드를 먼저 살펴보겠습니다.

val myList = listOf(1,2,3)
val targetList = mutableListOf<Int>()
for (i in myList) {
    if (i % 2 == 0) tempList.add(i)
}

로직이 간단하기 때문에, 코드를 순차대로 훑어 보시면 이 5줄의 코드가 어떤 의미를 가지는 코드인지 알 수 있습니다.

  1. 먼저 myList라는 숫자를 담는 list가 있습니다.
  2. targetList라는 임시 리스트를 만들었습니다.
  3. for문을 사용해서 myList의 모든 요소를 한 번씩 순회 합니다.
  4. 순회하면서 각 요소의 나머지가 0인 것들을 targetList에 담습니다.

컴퓨터에게 구체적으로 하나하나 명령한 이 코드를 따라가다 보니, 그제서야 list에서 짝수만 필터링하는 기능이라는 것을 알 수 있습니다. 우리는 방금 리스트에서 짝수만 필터링하는 그 과정, 즉 필터링을 하기 위해 어떻게(how) 하는지를 본 것입니다.

이같은 프로그래밍 방법이 명령형 프로그래밍입니다.

선언형 프로그래밍

반대로 선언형 프로그래밍은 다음과 같습니다.

val targetList = listOf(1,2,3).filter { it % 2 == 0 }

같은 필터링 기능인데, 세부 동작 하나하나를 명령한 느낌이 아닙니다. “나는 이 list에서 이것들을 필터링 할거야”라고 선언한 것에 불과합니다. 내부적으로 어떻게(how) 할지는 관심이 없습니다. 단지 “이렇게 표현할래”를 나타낸 것입니다.

우리는 컴퓨터가 아니라 사람입니다. 어렸을 때를 떠올려 보면, “엄마, 밥 먹고 싶어요”라고 표현했지, “엄마, 우선 밥솥에 쌀을 넣고, 물을 이만큼 넣은 후 밥솥에 전기를 연결해서 무슨 무슨 버튼을 눌러서..”이렇게 명령하지 않았습니다. 우리는 특정 개발자가 컴퓨터에게 명령한 로직들을 보고 그게 무슨 기능인지 이해하는 것보단, 개발자가 원했던 기능의 선언을 보고 이해하는 게 훨씬 익숙합니다. 그게 바로 선언형 UI가 대세가 된 이유라고 생각합니다.

선언형 프로그래밍은, 결국 잘 된 추상화라고도 볼 수도 있습니다. filter 함수도 내부 로직을 타고 들어가면 결국엔 컴퓨터에게 명령을 하는 코드로 되어있습니다. 다만 그걸 잘 추상화 하는 게 참 어려운 일이고, 그게 특히나 단순 로직의 추상화가 아니라 UI를 그리는 영역이라면 더더욱 어렵습니다. 누군가 그 어려운 일을 다 해준다면 우리는 컴퓨터에게 원하는 UI를 선언하기만 하면 됩니다(고맙게도 Jetpack Compose가 그 어려운 일을 다 해주고 있습니다👍🤩👍).

명령형 UI 프로그래밍

명령형 UI 프로그래밍은 아래와 같습니다.

val linearLayout = LinearLayout()
linearLayout.orientation = VERTICAL

val textView1 = TextView().apply {
    text = "hello"
}
val textView2 = TextVew().apply {
    text = "world"
}
linearLayout.add(textView1)
linearLayout.add(textView2)

아까 for 문을 돈 것과 비슷하게, 컴퓨터에게 명령을 내리고 있습니다. 코드를 다 보고, 어디에 무엇이 add 되는지 다 읽고 나면, “아~ textView 두 개가 위아래로 배치되어 있도록 그리는 코드이구나”라는 걸 이해하게 됩니다.

선언형 UI 프로그래밍

이번엔 같은 기능을, 명령하지 않고 선언해보겠습니다.

<LinearLayout
    ...
    android:orientation="vertical">
    <TextView
				...
        android:text="hello"/>
    <TextView
        ...
        android:text="world"/>
</LinearLayout>

제가 원하는 UI를 그저 표현했습니다. 어떤 UI가 될지 아까보다 훨씬 유추하기 쉽습니다.

사실 XML도 선언형이기 때문에 Android 개발자라면 이미 선언형 UI에 익숙하신 겁니다. 다만 XML 파일에 선언하였기 때문에 더 다이나믹하게 UI 자체를 바꾸는 건 굉장히 귀찮고 어렵습니다.

위의 코드를 컴포즈로 바꿔보면, 아래와 같습니다.

@Composable
fun MyView() {
    Column {
        Text("hello")
        Text("world")
    }
}

만약 다른 뷰로 바꾸고 싶으면, 그저 다른 Composable 함수를 호출하면 그만입니다. 프레그먼트를 사용해서 바꿀 필요가 없습니다.

마지막으로는 상속 대신 확장이라는 측면에서 살펴보려고 합니다. 이는 Compose API를 사용하는 측면에서는 크게 중요한 내용은 아닐지라도 충분히 살펴볼 가치가 있어서 정리해보았습니다.

3. 상속을 버리고 합성을 선택하다.

기존에는 모든 컴포넌트가 View.java를 상속해야 합니다. 그러나 View.java는 3만 줄이 넘을 만큼 너무 거대합니다. BaseClass가 이렇게 많은 역할을 하게 되면, 하위 클래스가 많아지면 많아질수록 Base 클래스 자체를 바꾸는 게 굉장히 부담으로 다가옵니다. 실제로 Android Team의 Anna-Chriara는, 이제 와서 이렇게 비대해진 상속 관계를 도저히 바꾸기 힘들다며 후회하고 있다고 말했습니다.

Jetpack Compose는 상속 대신에 합성(Composition)을 사용하여 전체적인 UI 컴포넌트를 구성하도록 설계되었고, Composable이라 불리는 각 UI 컴포넌트들 또한 전혀 상속을 하지 않고 합성만을 사용해서 구현되어 있습니다. 생각해 보면 Composable은 단지 상태를 받아서 UI를 그려주는 함수에 불과하기 때문에 당연히 상속이란 개념이 없는 게 당연합니다.

Jetpack Compose의 탄생은 단지 선언형 UI 개발에 대한 열망 때문만이 아닙니다. 위에서 말했듯이 Android Team의 과도한 상속 기반 API를 바로잡을 때가 되었기 때문이기도 합니다. 개인적으로는 Android 팀이 UI 툴의 전체적인 아키텍쳐를 상속이 아니라 합성을 기반으로 변경한 것이 굉장히 큰 결심이자 발전이라고 생각하여 이 부분을 조금 더 상세하게 들어가 보려고 합니다.

왜 Android UI System은 망했을까?

처음 Android가 나왔을 때는 사용자의 요구 사항이라던지, 하드웨어의 성능 문제로 딱히 View의 다양성이 필요하지 않았습니다. 그러나 시대가 흐를수록, 사용자의 요구 사항과 하드웨어 성능이 높아져서 더 많은 View 들과 함께 각 UI 컴포넌트들에 다이나믹한 동작들을 기대하게 되었습니다. 그래서 점점 UI들이 추가되다 보니, UI 컴포넌트라면 공통적으로 사용되는 UI의 속성들을 재사용하기 위해 View라는 Base Class를 만들었고, 이를 여기저기서 상속하게 되었습니다. 아마도 시간이 지날수록 계속해서 추가 기능들이 추가되었을 것이고, 아래와 같은 트리 형태가 나타나게 되었습니다.

image

ButtonText를 랜더링 하는 자신만의 방법을 굳이 따로 가질 필요 없이, TextViewText를 랜더링 하는 방식을 재사용 할 수 있어서 효율적입니다. 그러나 시스템이 커지면 커질수록, Android의 인기가 높아지면 높아질수록, 상속의 한계는 점점 더 명확하게 나타납니다. 예를 들어 텍스트가 아니라 이미지가 랜더링되는 Button을 사용하고 싶어졌다고 해봅시다. 아마도 Button을 상속한 ImageButton이란게 있지 않을까?라는 생각으로 ImageButton를 찾아봅니다. 역시나 ImageButton 클래스가 존재했고, XML에서 기존의 ButtonImageButton으로 교체합니다. 그러나 ImageButtonButton이니까 당연히 아무런 문제 없이 돌아갈 거라고 생각했지만, 실행조차 되지 않고 컴파일 에러가 납니다😭. 에러는 기존에 코틀린으로 findViewById를 사용하여 Button을 찾아서 Button의 메서드를 호출한 부분에서 발생하였습니다. ImageButtonButton인데 왜 Button의 메서드를 호출하는 코드에서 에러가 난걸까요? 그 이유는, 사실 ButtonImageButton이 굉장히 밀접한 관계가 있을거라 생각했지만 사실은 전혀 관계가 없기 때문입니다😞.

image

여기서부터 과도한 상속으로 인한 딜레마에 빠지게 되고, 안드로이드 UI 시스템은 정확히 이 덫에 걸렸다고 생각합니다. 이 현상은 스스로 한 번씩 고민해 볼 만한 현상이라고 생각합니다. 과연 우리라면 ImageButtonImgaeView의 자식으로 채택해야 할까요, Button의 자식으로 채택해야 할까요? 아니면 각각 따로 상속받는 ImageButton을 디자인해서 구현해야 할까요? 정답이 없는 문제지만 한 가지 얻을 수 있는 교훈은 있습니다. 자신이 만든 컴포넌트를 다른 개발자들이 어떻게 사용할 것인지는 예측할 수 없고, 이 복잡함을 한방에 해결할 수 있는 상속 관계를 설계하기는 더더욱 어려운 점이라는 것입니다.

잠시 돌아가서, 왜 ButtonTextView의 하위 클래스로 설계하였을까요? 구글 Android 팀이 추후에 ButtonImageView가 들어가리라고는 생각하지 못하고 저렇게 설계한 걸까요? 이유는 알 수 없지만 그런 모든 가능성을 예측해서 상속 계층을 디자인하는 건 구글 개발자들도 힘들다는 게 결론이 아닐까 싶습니다.

상속보다는 합성을

결국 객체지향의 기본 내용인데, 상속은 코드 재사용을 목적으로 사용해선 안됩니다. 상속은 타입 계층을 만들 목적으로 사용해야 합니다. 만약 코드 재사용을 목적으로 상속을 사용한다면 기존 Android UI System 같은 문제가 생길 수 있습니다. 계층 관계가 전혀 없이 단지 코드 재사용이 목적이라면, 상속보다는 해당 기능을 수행할 다른 객체에 의존해서 그 객체를 사용하도록 하는 게 좋습니다.

더 이상 Button에서 text를 변경할 때, 상속받은 text 속성에 “hello”를 입력하지 않아야 합니다. 대신 ButtonText를 가지고 있게 해서, Text가 랜더링 하도록 위임하면 됩니다. 그리고 이게 바로 Compose가 설계된 방식이고, 여러분이 Jetpack Compose로 UI를 그릴 때 사용할 방식입니다.

Button(onClick = { /*TODO*/ }) {
    Text(text = "Hello")
}

Compose 관련 포스팅인데 글이 이상하게 조금 흘렀네요..😅 상속과 합성에 대해 더 관심있으신 분은 https://kt.academy/article/ek-composition 글 정독을 추천드립니다.

Android는 다른 플랫폼에 비해 선언형 UI ToolKit 도입이 많이 늦은 편입니다. 늦은 만큼 좋은 UI toolkit(React.js, Flutter 등등)을 많이 참고했다고 하니 충분히 기대해봐도 좋을 것 같고, github에 이미 꾀나 훌륭하게 만들어진 샘플들도 많으니 참고하셔서 학습하시면 많은 도움이 될 것 같습니다.

참고했던 글

  • https://louis993546.medium.com/mental-models-of-jetpack-compose-1-state-programming-models-cc0d47209720
  • http://intelligiblebabble.com/compose-from-first-principles/
  • https://medium.com/androiddevelopers/understanding-jetpack-compose-part-1-of-2-ca316fe39050
  • https://medium.com/mateedevs/bye-xml-it-was-nice-knowing-you-pt-1-50b195bab1a9
  • https://proandroiddev.com/why-do-we-need-jetpack-compose-d69a5fd20122
  • https://betterprogramming.pub/deep-dive-into-jetpack-compose-b09713760019
  • https://velog.io/@tura/android-jetpack-jetpack-compose-part-1-concepts-backgrounds
  • https://danielebaroncelli.medium.com/the-future-of-apps-declarative-uis-with-kotlin-multiplatform-d-kmp-part-1-3-c0e1530a5343

첫 퇴사와 이직을 앞두고

|

2021년 5월 4일 날짜로 퇴사를 하게 되었습니다. 새로운 곳에서 새로운 도전을 하기 위해 두 달 가량 채용 프로세스를 밟았고, 운이 좋게도 제가 평소 자주 사용하는 서비스를 운영하는 조직, 평소 많이 궁금했고 관심이 많이 갔던 조직에 합격하여 현재 회사를 떠나게 되었습니다😊

이직 준비에 몰두하느라 제대로 된 글도 못썻고, 저 자신을 돌아볼 시간 없이 폭풍 치는 기술 면접 질문과 코딩 테스트를 헤쳐나가기 바빴던 것 같습니다. 이제야 조금 잠잠해진 이때 즈음 지난 날과 앞으로의 날을 생각해 볼 겸 키보드를 두들겨 보려 합니다🤭

2년 3개월의 짧은 경력을 보내며 들었던 생각

저는 2019년 2월에 Android로 처음 커리어를 시작한 (아직도)주니어입니다. 사직서에 총 경력을 적는 란이 있어서 쓰다 보니, 퇴사 기준으로 총 경력이 2년 3개월 밖에 되지 않더군요. 회사 규모가 나름 큰 편이라 사람이 많은데도 불구하고 2년 3개월 만에 퇴사하는 사람은 그리 흔치 않은 것 같습니다. 많은 것을 배우고, 많은 사람들을 만났다고 생각했지만 아직 병아리였네요🐣.

그래도 2년 넘는 시간 동안 하루하루 허투루 보낸 날은 없었기에 그동안 열심히 배우고 느꼈던 점들을 정리해 보려 합니다. 멋 훗날 시니어 개발자가 된 제가 첫 이직을 겪고 적었던 이 글을 보면 되게 재밌지 않을까요? ^__^

1. 최고의 복지는 최고의 동료

이런 생각을 가지게 된 시점이 정확히 기억나네요. 저는 아쉽게도 입사 직후 1년 동안 혼자 Android 개발을 했습니다. 15명 정도로 구성된 팀에 소속되어 있었지만, 저 혼자 Android 개발자였습니다. 다들 너무 좋으신 분들이라 조직에 잘 적응했고 분위기도 정말 좋았지만 혼자 개발하는 것에 대한 아쉬움은 있었습니다.

1년이 지난 후 모바일 개발팀이 새로 생기고, 새로 합류하신 분들, 사내 다른 팀에서 모바일을 개발하시던 분들이 한자리에 모이게 되었고, 함께 개발하기 시작했습니다. 그때 부터 최고의 복지는 최고의 동료라는 생각이 들었던 것 같아요. 물론 혼자 개발했던 1년 동안에도 재미있었지만, 좋은 사람들과 함께 개발하게 된 나머지 시간들은 훨씬 훨씬 즐거웠습니다. 신기하게도 팀원들 모두가 활기 넘치는 분들이었고, 수평적인 마인드(아니였다면 너무 죄송합니다;;)의 소유자였기에 출근하는 날이 매번 즐거웠던 기억이 납니다.

여기서 최고의 동료라는 표현은 지극히 주관적인 표현인데요, 개인의 기술력, 인성, 취미, 관심사 등등을 떠나서, 함께 있을 때의 시너지가 팀 성장에 긍정적인 영향을 미친다면 최고의 동료라고 생각합니다. 돌이켜보면 저희는 서로가 서로 부족한 점을 잘 알고 케어해주었던 것 같습니다. 저 같은 경우는 주니어다 보니 저희 서비스의 도메인 지식이 많이 부족해서 귀찮게 물어본 적이 많았는데요, 바쁜 시간 속에서도 모두들 귀찮은 내색 없이 전부 친절히 알려주셨고, 저 역시 기술적인 부분으로 질문이 들어오면 귀찮아하지 않고 도와주었던 것 같습니다. 아무래도 이런 점이 팀 성장에 긍정적인 영향을 미치지 않았을까 싶습니다. 물론 어디에도 완벽한 팀은 없기에 채워나가야 할 부분도 많이 남아있는 팀이었지만, 적은 인원과 여러 가지 상황을 고려해보면 훌륭한 팀이라고 생각합니다.

떠날 때 가장 눈에 아른거리고 망설이게 하는 것이 바로 최고의 복지라고 생각합니다. 아마도 저한테 최고의 복지는 짧은 시간이었지만 함께한 동료들이지 않았나 싶습니다.

정말 즐거웠습니다!

goodbye

2. 문화라고 착각했던 것, 그래도 값진 것

입사를 하고 나서 몇 주 되지 않았을 때 일입니다. 팀원이 15명 정도 있었는데, 한 분이 기술 세미나를 하기 위해 저희 팀 모두를 회의실로 초대했고, 곧바로 Spring 라이브 코딩 30분 정도 했었습니다. 누구 앞에서 발표하는 것이 두렵고, 특히나 내 코드를 누군가에게 보여준다는 것이 덜컥 겁이 나는 신입 시절에 보았던 그 라이브 코딩 세미나는 충격적이였습니다. 심지어 몇 달에 한 번, 짧을 때는 몇 주에 한 번씩 매번 본인의 지식을 공유하셨습니다. 특별히 발표 자료를 준비해온 게 아니라, 가볍게 intelliJ를 하나 켜놓고 본인의 생각을 코드로 표현하는 공유였습니다. spring을 몰랐기에 내용은 잘 몰랐지만 엄청난 자극이 되었고, 저도 저 대화에 끼고 싶다는 생각, 언젠간 저도 라이브 코딩 하면서 지식을 공유하고 싶다는 생각이 끊이질 않았습니다. 그래서 이제 막 코틀린을 조금 공부하고 나서 부랴부랴 어설픈 라이브 코딩 세미나를 했던 기억이 나네요😅. 아마 다른 분들도 저와 비슷한 자극을 받지 않았을까요?

아무튼 제가 일반적으로 봐왔던 형식적인 세미나(ppt를 만들어서 슬라이드를 넘기며 하는)가 아니라, 다소 부담 없이 본인의 생각을 코드로 보여주며 공유하는 것을 보면서 굉장히 좋은 문화구나라고 생각했었습니다. 그러나 시간이 많이 지나고 나서 깨달은건, 그건 그분의 빛나는 노력이었지 문화가 아니었습니다. 문화는 한두 사람이 주도해서 형성되는게 아니라, 거의 대부분의 미국 사람들이 집에서 신발을 신는 것과 같이 모두에게 자연스럽게 스며든 것이어야 합니다. 한두 사람이 포기해버리면 더 이상 이어지지 않는 건 문화라고 보기 어렵습니다.

image

세미나 까진 아니더라도, 저도 좋은 개발 문화가 되길 바라면서 시도한 것이 있습니다. 부문 내 모든 개발자들끼리 각자 공부하다가 발견한 좋은 자료를 서로 공유하고, 가능하다면 자극도 받을 수 있는 문화를 만들고자 개발 링크 공유방을 사내 메신저에 만들었는데요, 활발한 문화로 만드는 건 참 어렵더군요🧐. 비록 제가 링크를 올리지 않으면 많이 조용해지는 방이긴 했지만, 앞으로는 모두가 활발히 공유하는 메시지방이 되었으면 하는 바람이 있습니다 ㅎㅎ.

함께 성장하기 위한 좋은 개발 문화를 만드는 건, 실패하더라도 큰 값어치가 있다고 생각합니다. 소수의 인원이라도 저처럼 그 노력에 자극을 받는 사람이 생기고, 그 사람들이 또 좋은 문화를 위해 노력하다 보면, 본인의 성장은 물론이고 조금씩 조금씩 조직에 맞는 개발 문화가 생겨나지 않을까 생각합니다.

3. 비개발자가 바라보는 개발자 or 개발 조직

개발자는 절대 서비스가 런칭되었다고 해서 놀지 않습니다. 좋은 소프트웨어, 좋은 품질, 경쟁력 있는 IT 서비스가 되기 위해서는 결국 코드를 갈고닦아야 합니다. 추후에 있을 추가적인 요구 사항을 다른 경쟁 서비스보다 더 안정적이고 빠르게 반영하기 위해서 개발자는 늘 좋은 코드에 대해 고민하고, 리팩토링 하고, 더 안정적인 기술과 새로운 기술을 적용해야 합니다. 즉, 1차적으로 프로젝트가 런칭되었다고 해서 거기서 끝이 아닙니다. 계속해서 리팩토링을 고민하며 반영하는 사람이 필요하고, 새로운 기술을 학습해서 적용시킬 사람이 필요하며, 때로는 동료 개발자들에게 스파크가 될 사람이 필요합니다. 그리고 이게 바로 우리 서비스를 늘 빛나도록 갈고닦는 일이라고 생각합니다.

하지만 이를 비 개발자, 특히 프로젝트 투입 인력과 기간 결정권자가 비 개발자인 경우, 설득하기란 참 어려운 일인 것 같습니다. “우리 개발 조직에는 개발자 인원이 4명일 때와 8명일 때 차이가 수치적으로 이만큼 나니까 총 8명이 필요해요”라고 말하기가 참 어렵고 애매합니다. “신규 런칭이 끝나도 코드를 계속 갈고닦아야 하고, 추후의 요구 사항 변경을 준비할 사람들이 많이 필요해요”라고 말해도, 사실 외부적으로는 보이는 게 없기 때문에 설득하기가 참 어려운 것 같습니다.

제가 2년차에 접어들자마자 한 작업은 저희 부문의 주력 서비스를 리뉴얼 하는 작업이었습니다. 꽤나 큰 앱이었고, 코드량도 많은 프로젝트인데요, 리뉴얼을 하기 직전 코드가 상당히 레거시 코드였습니다. 당연히 MVP, MVVM도 아니었고, Kotlin도 아니었으며, 이를 유지보수 하는 분은 단 한 분이었습니다. 한 분이 그렇게 큰 프로젝트를 담당하다 보니 코드를 갈고 닦을 시간은 턱없이 부족한 상황입니다. 제가 느낀 첫 인상은, ‘한 5년전 코드 그대로구나’입니다. 놀라운 것은 그게 저희 주력 서비스라는 점이었습니다. 주력 서비스라면 당연히 계속 갈고 닦아야 했는데, 그러지 못해서 리뉴얼에 무려 1년이란 시간이 걸렸습니다. 조금 더 개발자를 붙여서 평소에 갈고 닦았으면 어땠을까요..?

회사마다 정책이 다르고, 전략이 다르니 좋다 나쁘다 딱 잘라 말하긴 어려운 부분이지만, 개인적으로는 조금 아쉬웠던 부분으로 남을 것 같습니다.

언젠가 다시 한번, 더 좋은 서비스를 위해 개발자 충원을 어필해야 한다면, 과연 어떻게 어필해야 좋을까요? 참 어려운 주제인것 같습니다.

앞으로의 날들

한 회사에 오래 있다고 하더라도, 1년 차 때 느낄 수 있는 것, 3년 차, 5년 차, 10년 차 이상일 때 느낄수 있고 배울 수 있는 것들이 다 다를 거라고 생각합니다. 짧은 2년 3개월이었지만, 1년 차, 2년 차, 3년 차가 되면서 느꼈던 점들이 많이 달랐고, 저는 그대로지만 주변 환경과 사람들이 바뀌면서 새롭게 느꼈던 점들도 많았습니다. 앞으로의 날은 이전 회사보다 더 오래 있으면서, 4년 차, 5년 차 때 느낄 수 있는 것들도 경험해보고 싶습니다. 물론 더 이상 제가 성장하기 힘든 환경이 되거나, 회사의 방향이 제가 생각 하는 방향과 많이 다르게 변한다면 또 한 번의 이직을 생각하겠지만, 그전까진 최대한 많은 경험을 쌓으며 즐겁게 일하고 싶습니다.

The one and only object(번역)

|

원본 - The one and only object

자바에서 static 키워드는 인스턴스화된 객체가 아닌, object와 관련된 메서드와 프로퍼티를 나타낼 때 쓰입니다. 그리고 굉장히 널리 쓰이는 디자인 패턴인 Singleton을 생성할 때도 쓰입니다. 싱글톤은 다른 여러 객체들에서 접근할 수 있는 오직 하나의 객체를 생성하는 것입니다.

코틀린은 싱글톤 패턴을 구현하기 위해 더 우아한 방법을 제공합니다. 바로 object 키워드입니다. Java와 Kotlin 이 Singleton을 구현하는 방법에 어떤 차이가 있는지, Kotlin에서 static 키워드를 사용하지 않고 어떻게 Singleton을 만드는지(먼저 말씀드리면 object 키워드를 사용합니다), 또 내부적으로 어떻게 동작하는지 궁금하시다면 끝까지 읽어주세요.

우선, 잠시 뒤로 물러나서 오직 단 하나의 객체인 Singleton이 왜 필요한지를 알아봅시다.

Singleton이란?

Singleton은 클래스가 단일 객체를 가지고 있고, 그 객체에 대해 global 접근이 가능한 패턴을 말합니다. 그래서 앱의 여러 부분에서 공유해서 사용할 객체이거나 객체를 생성할 때 많은 resource가 드는 경우에 유용하게 사용됩니다.

Singleton In Java

클래스가 단일 객체를 가지고 있음을 보장하려면 object 생성에 직접 관여를 해야합니다. 단일 객체를 가진 클래스를 만드려면, 클래스 생성자를 private으로 하고 단일 object에 대해 static하게 접근 할 수 있도록 만들어야 합니다. 한편 보통 싱글톤은 객체 생성 비용이 많이 들기 때문에 앱이 시작할 때 Singleton을 만들고 싶지는 않을 겁니다. 이런 상황을 위해 이미 생성된 객체가 있는지를 확인하는 static 메서드를 제공해줍니다. 이 메서드는 이미 생성된 instance가 있으면 그것을 반환하고, 없다면 새로 생성하여 반환합니다.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

public class Singleton{
    private static Singleton INSTANCE;
    private Singleton(){}
    public static Singleton getInstance(){
        if (INSTANCE == null){
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
    private int count = 0;
    public int count(){ return count++; }
}

위 코드는 겉보기엔 괜찮아 보일지 몰라도 thread-safe하지 못합니다. 하나의 쓰레드가 if문을 통과하였지만, 다른 쓰레드가 싱글톤을 생성하고 있었다면 작업을 잠시 멈추게 될 수 있습니다. 그러다가 다시 if문 안에서 resume되면, 그때는 또 다른 instance를 만들게 되겠죠.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

public class Singleton{
    private static Singleton INSTANCE;
    private Singleton(){}
    public static Singleton getInstance(){
        if (INSTANCE == null) {                // Single Checked
            synchronized (Singleton.class) {
                if (INSTANCE == null) {        // Double checked
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
    private int count = 0;
    public int count(){ return count++; }
}

이문제를 해결하기 위해 double checked locking을 사용할 수 있습니다. 이를 사용하면 instance가 null일 때, synchronized 키워드가 lock을 걸고, 두 번째 if문에서 instance가 정말로 null인지를 다시 체크합니다. 만약 instance가 null이라면 Singleton을 만듭니다. 그러나 이것 만으로도 충분하진 않습니다. instance를 volatile로 선언해줘야합니다. Volitile 키워드는 변수가 동시에 동작하는 쓰레드들로 인해 비동기적으로 수정될 수 있음을 컴파일러에게 알려줍니다.

이 모든것은 여러분이 싱글톤이 필요할 때마다 작성해야 하는 보일러 플레이트 코드입니다. 사실 간단한 작업임에도 불구하고 코드는 꽤나 복잡하네요. 그래서 자바에서는 대부분 enum을 사용하여 싱글톤을 만들기도 합니다.

Singleton in Kotlin

코틀린은 어떨까요? 코틀린은 static method와 static fields가 없는데, 그렇다면 어떻게 싱글톤을 만들까요?

안드로이드 스튜디오나 IntelliJ를 사용하면 이해하기가 좀 더 수월해집니다. Java로 만들어진 Singleton을 Kotlin으로 변환해보면, 모든 static property들과 메서드들은 companion object로 옮겨집니다.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

class Singleton private constructor() {
    private var count = 0
    fun count(): Int {
        return count++
    }

    companion object {
        private var INSTANCE: Singleton? = null// Double checked

        // Single Checked
        val instance: Singleton?
            get() {
                if (INSTANCE == null) { // Single Checked
                    synchronized(Singleton::class.java) {
                        if (INSTANCE == null) { // Double checked
                            INSTANCE =
                                Singleton()
                        }
                    }
                }
                return INSTANCE
            }
    }
}

우리의 예상대로 코드가 변환되었지만 이걸 좀 더 간단하게 변경할 수 있습니다. 간단하게 하기 위해서 생성자와 companion 키워드를 지우고 object 키워드를 클래스명 앞에 붙여주세요. objectcompanion objects의 차이는 글 아래에서 다루도록 할게요.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

object Singleton {
    private var count: Int = 0

    fun count() {
        count++
    }
}

count() 메서드를 사용하고 싶다면, Singleton object를 통해서 접근할 수 있습니다. 코틀린에서 object는 단일 instance를 가지는 특별한 클래스입니다. class 대신 object키워드를 사용하여 class를 만들면, 코틀린 컴파일러는 생성자를 private하게 만들어주고, object에 대한 static reference를 만들어주고, static block 안에서 reference를 초기화 해줍니다.

Static block은 static field에 처음 접근할 때 딱 한번만 호출됩니다. JVM은 static block을 처리할 때, 비록 synchronized키워드가 없다고 하더라도 synchronized block과 매우 비슷하게 다룹니다. Singleton 클래스가 초기화 될 때, JVM synchronized block위에서 lock을 획득하여 다른 쓰레드가 동시에 접근할 수 없도록 해줍니다. lock이 풀릴때면 Singleton 객체는 이미 생성이 되었고 static block은 두번 다시는 수행되지 않습니다. 이렇게 Singleton 객체가 단 하나만 있음을 보장하게 됩니다. 게다가 이 object는 thread-safe함과 동시에 object에 처음 접근하는 순간 생성됩니다(lazliy-created).

그럼 디컴파일된 Kotlin byte code를 살펴보고 실제로 어떤 일이 일어났는지 확인해봅시다.

Kotlin class의 바이트 코드를 확인하려면 Tools > Kotlin > Show Kotlin Bytecode 를 선택하세요. Kotlin byte code가 나오면, Decompile을 눌러 decompile된 java code를 확인할 수 있습니다.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

public final class Singleton {
   private static int count;
   public static final Singleton INSTANCE;
   public final int getCount() {return count;}
   public final void setCount(int var1) {count = var1;}
   public final int count() {
      int var1 = count++;
      return var1;
   }
   private Singleton() {}
   static {
      Singleton var0 = new Singleton();
      INSTANCE = var0;
   }
}

그러나 object도 한계가 있습니다. object 선언은 생성자를 가질 수 없기에 파라미터를 받을 수가 없습니다. 만약 파라미터를 받을 수 있다고 하더라도, 생성자에 전달된 non-static 파라미터는 static block에서 접근할 수 없기 때문에 의미가 없을 겁니다.

static 초기화 블록은 다른 static method처럼 오직 클래스의 static property에만 접근할 수 있습니다. Static block은 객체 생성 전에 호출되기 때문에 객체의 property나 생성자로 받아온 파라미터를 사용할 수 없습니다.

Companion object

companion object는 object랑 비슷합니다. companion object는 언제나 클래스 안에 선언되며 클래스의 프로퍼티에 접근할 수 있습니다. 또한 companion object는 굳이 이름이 필요없습니다. 만약 companion object에 이름을 부여하면, 호출하는 쪽에서 companion object의 이름을 사용하여 member에 접근할 수 있습니다.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

class SomeClass {
    //…
    companion object {
        private var count: Int = 0
        fun count() {
            count++
        }
    }
}
class AnotherClass {
    //…
    companion object Counter {
        private var count: Int = 0
        fun count() {
            count++
        }
    }
}
// usage without name
SomeClass.count()
// usage with name
AnotherClass.Counter.count()

예를 들면 위에 코드에는 이름이 있는 companion object와 이름이 없는 companion object가 있습니다. count()를 호출하는 쪽에서는 마치 SomeClass의 static 멤버인 것 처럼 count()를 사용할 수 있습니다. 또한 AnotherClass의 경우 static 멤버를 사용하듯이 Counter를 사용하여 count() 메서드에 접근할 수 있습니다.

companion object는 private 생성자가 포함된 inner class로 디컴파일 됩니다. host 클래스는 오직 자신만 접근할 수 있는 synthetic 생성자를 통해 inner class를 초기화 합니다. 그리고 다른 클래스에서 companion object에 접근할 수 있도록 public refernece를 유지합니다.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

public final class AnotherClass {
    private static int count;
    public static final AnotherClass.Counter Counter = new AnotherClass.Counter((DefaultConstructorMarker)null);

    public static final class Counter {
        public final void count() {
            AnotherClass.count = AnotherClass.count + 1;
        }
        private Counter() { }
        // $FF: synthetic method
        public Counter(DefaultConstructorMarker $constructor_marker) {
            this();
        }
    }

    public static final class Companion {
        public final void count() {
            AnotherClass.count = AnotherClass.count + 1;
        }
        private Companion() {}
    }
}

Object 표현식

지금까지 우리는 object keyword를 object 선언식으로만 살펴보았습니다. object keyword는 표현식으로도 사용될 수 있습니다. 표현식으로 사용하면, object keyword는 익명 객체와 익명 inner class를 생성할 수 있습니다.

잠시동안 어떤 값을 홀딩하고 있을 객체가 필요하다고 해봅시다. 객체를 선언하고, 원하는 값들과 함께 초기화 하여 추후에 접근할 수 있습니다.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

val tempValues = object : {
    var value = 2
    var anotherValue = 3
    var someOtherValue = 4
}

tempValues.value += tempValues.anotherValue

generated code를 보면, 위의 코드는 으로 표기된 익명 Java class로 변환되어 getter와 setter와 함께 익명 객체가 저장됩니다.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

<undefinedtype> tempValues = new Object() {
    private int value = 2;
    private int anotherValue = 3;
    private int someOtherValue = 4;

    // getters and setters for x, y, z
    //...
};

또한 object keyword는 boilerplate code 없이도 익명 클래스를 생성할 수 있도록 도와줍니다.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

val t1 = Thread(object : Runnable {
    override fun run() {
         //do something
    }
})
t1.start()

//Decompiled Java
Thread t1 = new Thread((Runnable)(new Runnable() {
     public void run() {

     }
}));
t1.start();

object keyword는 thread-safe한 singleton을 생성하도록 도와줄 뿐만 아니라, 익명 객체와 익명 클래스를 별도의 추가 코드 없이 생성하도록 도와줍니다. object와 companion object를 사용하면 코틀린은 static keyword를 사용한 것과 동일한 기능을 하도록 코드를 생성해줍니다. 게다가 object를 표현식으로 사용하면 boilerpalte code 없이도 익명 객체와 익명 클래스를 만들 수 있습니다.

Explore Kotlin Annotations(번역)

|

원본 - Explore Kotlin Annotations

(자바와 함께 사용할 때 도움이 되는 Kotlin annotation들)

이번 포스팅에는 Android app을 개발할 때 쓰이는 Kotlin annotation들을 살펴보겠습니다. 그 중에서도 @JvmStatic, @JvmOverloads, @JvmField에 대해 알아볼게요.

구글이 Android 개발 언어를 Kotlin으로 공식 지정한 이후로, 많은 개발자 분들이 Java를 Kotlin으로 변경하고 있습니다. 그렇지만 한순간에 모든 Java 코드를 Kotlin으로 전환할 수는 없죠.

그래서 코드의 일부(Class, enum 등등)들은 Kotlin으로 전환하되, 일부는 java와 혼합해서 사용될 수 있습니다. 만약 여러분이 이런 상황에 놓여있다면, 아래에서 다룰 annotation들을 알아두셔야 해요.

@JvmStatic

코틀린에서는 pakage-level function이 static method로 표현됩니다. 또한 @JvmStatic annotation을 사용하여 일부 companion object나 이름있는 object에 정의된 함수에 대한 정적 메서드를 만들 수도 있습니다.

@Target([AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER]) annotation class JvmStatic

이 어노테이션이 함수에 적용된다면, 함수에 대해 추가적인 static method가 생성됩니다. 만약 프로퍼티에 적용된다면, 추가적으로 static getter/setter 메서드가 생성됩니다.

아래의 코드를 살펴볼게요.

class ClassName {
    companion object {
        @JvmStatic
        fun iAmStatic() {
            //body of Static function
        }
        fun iAmNonStatic() {
            //body of Non static function
        }
    }
}

iAmStatic()은 static function이고, iAmNonStatic()은 non-static function입니다. Java class에서 위의 메서드를 호출한다고 생각해보세요. 그 결과는 아래와 같습니다.

ClassName.iAmStatic(); // works fine
ClassName.iAmNonStatic(); // compile error
ClassName.Companion.iAmStaticMethod(); // works fine
ClassName.Companion.iAmNonStaticMethod(); // other way to work

이름있는 object의 경우에도 똑같이 적용됩니다.

object ObjectName {
    @JvmStatic fun iAmStatic() {
        //body of Static function
    }
    fun iAmNonStatic() {
        //body of Non static function
    }
}

Java에서 호출하면 결과는 아래와 같습니다.

ObjectName.iAmStatic(); // works fine
ObjectName.iAmNonStatic(); // compile error
ObjectName.INSTANCE.iAmStatic(); // works
ObjectName.INSTANCE.iAmNonStatic(); // works

@JvmOverloads

@Target([AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR]) annotation class JvmOverloads

이 어노테이션이 적용되면, 해당 함수의 default parameter 값을 대체하도록 컴파일러가 overloads를 생섭합니다. 만약 메서드가 N개의 파라미터를 가지고 있고, 그 중 M개가 default value를 가지고 있다면, M개의 overloads가 생성되는 것이죠. N-1개의 파라미터를 가진 것(default value 파라미터중 마지막 하나를 제외함) 한 개, N-2개의 파라미터를 가진 것 한 개, … 이렇게 결국 모든 overloads가 생성됩니다.

여기 두 개의 필드를 가진 클래스가 있습니다. 필드 하나는 초기화 되었고, 다른 하나는 그렇지 않습니다.

data class Customer( val name: String, val dob: Date = Date())

Customer 객체를 생성할 때, 두 번째 argument로 어떠한 값도 넘기지 않으면 현재 시간이 적용될 것입니다. 따라서 이를 코틀린에서 호출할 때는 별다른 컴파일 에러 없이 아래 코드로 수행할 수 있습니다.

val custOne = Customer("Don")
val custTwo = Customer("Don", Date())

Java에서 동일하게 호출하면 어떻게 될까요? Java에서는 모든 파라미터를 꼭 입력해주어야 합니다. 그렇지 않으면 컴파일 에러가 발생해요.

Customer custOne = new Customer("Don"); //Here we are passing only one argument, so we will get compile error
Customer custTwo = new Customer("Don", new Date());//No error

Java에서 호출할 때 역시 default value를 사용하려면, kotlin 코드에 @JvmOverloads를 적용시켜야 합니다. 어노테이션을 적용한 kotlin 코드는 아래와 같습니다.

Customer @JvmOverloads constructor( val name: String, val dob: Date = Date())

이제 Java에서 호출할 때도 모든 파라미터를 넘길 필요가 없어집니다.

Customer custOne = new Customer("Don"); //No Error
Customer custTwo = new Customer("Don", new Date());//No error

@JvmField

@Target([AnnotationTarget.FIELD]) annotation class JvmField

이 어노테이션이 적용되면, 컴파일러는 해당 property에 대한 getters/setters를 생성하지 않고 field자체로 노출시킵니다.

아래에는 일반적인 Java코드가 있습니다.

public class Customer {
    public String name;
    public Date dob;

    public getName() {
        return this.name;
    }
    public getDob() {
        return this.dob;
    }
}

이에 대응되는 Kotlin 코드는 아래와 같습니다.

data class Customer( val name: String, val dob: Date = Date())

만약 위 클래스의 프로퍼티에 접근하려면, 코틀린에서는 아래와 같이 접근하죠.

val customer = Customer("Don", Date())
val name = customer.name
val dob = customer.dob

그러나 자바에서는 아래처럼 getter 메서드를 사용해야 합니다.

Customer customer = new Customer("Don", new Date());
String name = customer.getName();
Date dob = customer.getDob();

만약 여러분이 특정 field에 대한 getter, setter를 원하지 않고 그저 일반적인 field로 사용하고 싶다면 @JvmField를 사용하여 컴파일러가 getter, setter를 생성하지 않도록 할 수 있습니다. @JvmField 어노테이션을 적용한 kotlin 코드는 아래와 같습니다.

data class Customer(@JvmField val name: String, val dob: Date = Date())

이제 Java에서도 아래처럼 field에 접근할 수 있습니다.

Customer customer = new Customer("Don", new Date());
String name = customer.name;

감사합니다.