박상권의 삽질블로그

코틀린(Kotlin), 꼭 해야하나요? 본문

IT/Android-TIP (한글)

코틀린(Kotlin), 꼭 해야하나요?

박상권 2019. 5. 15. 22:03

안드로이드 개발자들이 모여있는 오픈채팅방에 참여해보세요 .
Q&A 및 팁을 공유하는 방입니다..
오픈채팅방 참여


블로그를 Medium으로 옮겨서 운영하고 있습니다.
앞으로 새로운 글은 모두 미디엄 블로그를 통해서 올릴 예정입니다.
미디엄에서 다양하고 유익한 포스팅을 살펴보세요
미디엄 블로그 보기


이번에 '클린 아키텍처'를 주제로 온라인 강의를 개설하게 되었습니다 🎉
평소 오픈채팅방이나 여러 커뮤니티에서 '클린 아키텍처'와 관련된 질문들이 많았는데요. 이를 해결해줄 수 있는 마땅한 강의가 없었던것 같습니다.
평소 '클린 아키텍처' 에 대한 궁금증이나 관심이 있으셨던 분들이 수강해보시면 도움이 될것 같아요
강의 살펴보기


이 글은 2019년4월5일 있었던 '드로이드나이츠 2019'행사에서 발표했었던 내용에 기반한 포스팅입니다.

제 목소리와 함께 영상으로 시청하실분들은 아래 링크를 통해 영상으로 감상해보세요

https://www.youtube.com/watch?v=e9O0wt-eY-E


 

"코틀린, 꼭 해야하나요?

코틀린이 나온 이후 몇년전부터 여러 오픈 채팅방과 커뮤니티에 끊임없이 주기적으로 올라오는 질문들입니다.

 

사실 저는 “코틀린 꼭 해야하나요?” 라는 질문을 “코틀린 하기 싫어요” 라고 읽습니다.

이미 할 사람이라면 이미 학습을 시작 했을것입니다.

이런 질문을 하시는분들은 하기는 싫은데 하는사람은 많고 안할 이유를 찾고 싶은 답정너 느낌입니다.

 

자매품으로는 아래와 같은 질문들이 있습니다.

- “데이터바인딩 꼭 해야하나요?”
- “RxJava 꼭 해야하나요?”
- “MVP, MVVM으로 꼭 해야하나요?”

 

 

TL; DR ( Too Long; Didn’t Read)

네, 하세요 꼭 하세요 
두번 하세요

Google ♥ Kotlin

2017년 구글 I/O에서 코틀린은 공식언어로 지정되었습니다.

 

 

또한 2019년 구글 I/O에서도 다시한번 코틀린을 언급하면서 Kotlin First 시대를 알리고 구글에서 만드는 라이브러리들은 Kotlin으로 배포할것임을 알렸습니다.

 

 

 

안드로이드 공식 문서를 보시면서 많이 느끼셨겠지만 예제코드의 메인 언어가 코틀린으로 짜여져 있습니다.

예전에는 Java코드밖에 없었는데 어느날 Kotlin탭이 추가되더니 요즘에는 아예 Kotlin탭이 더 먼저 나오기 시작했습니다.

 

참고로 안드로이드 공식문서는 무조건 영어로, 한글로 보면 안됩니다.

한글번역이 많이 늦어서 영어로는 변경이 되었으나 한글로는 해당 내용이 보이지 않는 불상사가 발생합니다.

 

 

Java vs Kotlin

코틀린을 사용했을때 자바언어와의 비교, 특히 코틀린으로 안드로이드로 개발했을때의 차이에 대해서 비교하면서 설명해보겠습니다.

 

사전 지식

시작하기에 앞서 가장 기본적인 사전지식 몇가지를 소개해드리고 시작하겠습니다.

@Nullable
private String address;
@NonNull
private String id;

Java에서의 Nullable과 NonNull 변수들을 코틀린으로 변환하면 다음과 같습니다.

(NonNull은 support library 19.1 부터 추가된 어노테이션이며 그외 @StringRes, @ColorRes 등 유용한것들도 많습니다.)

 

 

var address: String? = null
var id: String = “ted”

Nullable한 변수는 Type에 물음표를, 그렇지 않은 변수는 없이 선언해줍니다.
또한 수정가능한 변수라면 var로 선언합니다.

 

val birthYear: Int = 1986

Val 의 경우 한번 할당하면 변경 할 수 없습니다.

val birthYear = 1986

타입선언없이 바로 값 할당이 가능합니다.

 

 

 

헤이딜러

뜬금없이 헤이딜러가 나와서 당황하셨죠..?
앞으로 나오는 예제코드들을 저희 회사 서비스에서 쓰이는 개념으로 예를 들어보겠습니다.
헤이딜러는 중고차를 딜러에게 팔고자 하는사람에게 여러명의 딜러 견적을 비교해서 선택하고 판매할 수 있도록 도와주는 서비스입니다.

 

예시

 

사용자가 차량 판매 견적을 올리고 경매가 시작된뒤 딜러들이 견적을 올리고 사용자가 판매하고 싶은 딜러를 선택한 상황이라는 예시로 들어보겠습니다.

이 상황의 경우 이와같은 클래스들과 서로의 관계를 갖게 됩니다.

거래정보(Car) 
-> 경매결과(Auction)
-> 선택된 견적(Bid)
-> 해당 견적의 딜러(Dealer)
-> 딜러의 이름 가져오기(String)

 

Safe call

private String getSelectedDealerName(@Nullable Car car) {
    if (car == null) {
        return null;
    }
    Auction auction = car.getAuction();
    if (auction == null) {
        return null;
    }
    Bid selectedBid = auction.getSelectedBid();
    if (selectedBid == null) {
        return null;
    }
    Dealer dealer = selectedBid.getDealer();
    if (dealer == null) {
        return null;
    }
    return dealer.getName();
}

그런 클래스 구조에서 경매정보를 서버에서 받아와서 선택받은 딜러의 이름을 화면에 표시하기위해서 이름을 가져오게 된다면 위와 같이 표현할 수 있을것입니다.

이때 값을 가져올때마다 if() null 체크를 항상 해주어야 합니다.

대단히 번거롭습니다.

 

코틀린을 사용한다면 아래와 같이 표현할 수 있습니다.

private fun getSelectedDealerName(car: Car?): String? {
    return car?.auction?.selectedBid?.dealer?.name
}

어때요 참 쉽죠?

 

물음표를 이용해서 null이 아니라면 다음 변수를 참조할 수 있도록 체이닝할 수 있습니다.
이러한 개념을 safe call이라고 합니다.

 

 

Class

public class Dealer {
    @Nullable
    private String name;
    @Nullable
    private String address;

    public Dealer(@Nullable String name, @Nullable String address) {...}

    @Nullable
    public final String getName() {...}

    public final void setName(@Nullable String var1) {...}

    @Nullable
    public final String getAddress() {...}

    public final void setAddress(@Nullable String var1) {...}
}

자바에서 딜러정보 클래스를 만든다면 위와 같이 만들게 될것입니다.
물론 IDE의 도움을 받아서 필드만 선언해주고 setter(), getter()를 만들어주면 되므로 나름대로의 개선이 있었습니다.
하지만 여전히 코드량이 많고 불편한건 사실입니다.

 

코틀린으로 만들경우 다음과 같이 만들면 끝입니다.

data class Dealer(var name: String?, var address: String?)

var를 선언해서 setter/getter가 자동생성되었고, 물음표를 통해서 Nullable한 필드임을 알 수 있습니다. 또한 생성자도 만들어 줍니다.

 

심지어 toString(), hashCode(), equals(), copy()까지 모두 알아서 만들어줍니다.(data class 기준)

 

 

 

String template

만약 Log를 찍기 위함이거나 텍스트를 만들때 이와 같이  a + b + c 와 같은 방식으로 String을 만들어줄것입니다.

private void logDealerInfo(@NonNull Dealer dealer) {
    String name = dealer.getName();
    String address = dealer.getAddress();
    Log.d("ted", "딜러이름: " + name + " , 딜러주소: " + address);
}

혹은 조금은 더 개선된 방식으로 String format을 이용해서 간결한 코드로 작성하기도 합니다.

String.format(Locale.getDefault(), "딜러이름: %1%s , 딜러주소: %2$s", name, address)

코틀린을 사용하면 아래와 같이 $ 표시로 바로 변수를 넣어주면서 String을 만들어 줄 수 있습니다.

private fun logDealerInfo(dealer: Dealer){
    val name = dealer.name
    val address = dealer.address
    Log.d("ted","딜러이름: $name , 딜러주소: $address")
}

 

 

Overload

오버로딩을 지원해서 생성자나 함수를 만들어야하는 경우도 있습니다.
특히 커스텀뷰를 만들때 아래와 같이 여러 생성자를 오버로딩할 수 있도록 제공해야합니다.
커스텀뷰를 만들때 거의 기계적으로 구현해주는 생성자들 입니다.

public class TedCustomView extends FrameLayout {
    public TedCustomView(@NonNull Context context) {...}

    public TedCustomView(@NonNull Context context, @Nullable AttributeSet attrs) 
{...}

    public TedCustomView(@NonNull Context context, @Nullable AttributeSet attrs,    
int defStyleAttr) {...}

    public TedCustomView(@NonNull Context context, @Nullable AttributeSet attrs, 
int defStyleAttr, int defStyleRes) {...}

}

 

하지만 코틀린을 사용한다면 간결한 코드로 만들 수 있습니다.

class TedCustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) :
    FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
    ...
}

= null, = 0 과 같이 만약 해당값이 없다면 default로 이러한 값들이 들어가도록 지정하면 알아서 오버로딩 생성자가 만들어집니다.

 

 

 

Lambda

버튼 클릭 리스너를 만들어줄때 다음과 같이 정의하시게 될겁니다.

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        // 할일하기
    }
});

물론 요즘 대부분 Java8을 사용하기때문에 이와 같이 람다식으로 선언해주는 경우가 더 많을것입니다.

button.setOnClickListener(view -> {
    // 할일하기
});

그래도 여전히 거의 사실상 쓰이지않는 view를 인자로 받아야하고 __ 과 같은 ignore이름으로 선언하곤 합니다.

 

하지만 코틀린을 사용하면 아래와 같이 사용가능합니다.

button.setOnClickListener {
    // 할일하기
}

사용되지 않는 view마저도 없애버릴 수 있습니다.

 

 

 

 

let

딜러 정보를 파라미터로 받아서 null이 아닌지를 체크하고 이름을 가져와서 활용하게 된다면 자바의 경우 이와 같이 코딩하게 됩니다.

private void setDealerName(@Nullable Dealer dealer) {
    if (dealer != null) {
        String name = dealer.getName();
        // 할일하기
    }
}

 

이때 코틀린에서는 let을 사용합니다.

아까 소개했었던 물음표로 null을 체크하면서 let을 함께 붙여주면 딜러가 null이 아닐때만 이 스코프로 들어오게 되고 그안에서 할일을 해주면 됩니다.

private fun setDealerName(dealer: Dealer?) {
    dealer?.let { dealer ->
        val name = dealer.name
        // 할일하기
    }

}

자바에서의 코드보다 더 직관적이고 간결한 코드로 작성할 수 있습니다.

심지어 람다 인자까지도 없앨수 있고 그럴경우 들어오는 값은 암묵적으로 it으로 사용할 수 있게 됩니다.

 dealer?.let { 
        val name = it.name
        // 할일하기
    }

 

 

 

let + run

딜러정보로부터 이름을 가져와서 사용하는데 
만약 딜러정보가 null이거나 이름이 null이면 문제가 발생하고 
이때 오류리포팅을 하거나 로그를 남기도록 하는 코드를 작성하고자 한다면 다음과 같이 만들 수 있습니다.

private void setDealer(@Nullable Dealer dealer) {
    if(dealer == null){
        // 문제발생
    }else {
        String name = dealer.getName();
        if(name == null){
            // 문제발생
        }else{
            // 할일하기
        }
    }
}

이름 좀더 간결한 코드로 줄여보고자 한다면 다음과 같이 바꿀 수 있을것입니다.

private void setDealer(@Nullable Dealer dealer) {
    if (dealer != null && dealer.getName() != null) {
        String name = dealer.getName();
        // 할일하기
    } else {
        // 문제발생
    }
}

이때 let과 elvis 오퍼레이터, run으로 다음과 같이 구성할 수 있습니다.

private fun setDealer(dealer: Dealer?) {
    dealer?.name?.let {
        // 할일하기
    } ?: run {
        // 문제발생
    }
}

(아시는분들은 아시겠지만 이 방식에는 구멍이 존재합니다.

let의 마지막 코드의 결과가 null인경우 의도하지 않게 run의 block으로 빠지게되어 의도하지 않은 동작을 유발시킬수 있습니다.

하지만 보통은 그런 케이스가 발생하지 않아서 문제없이 사용하곤 합니다.)

 

 

 

 

apply

이번에는 딜러 정보를 설정하는 코드를 보겠습니다.
딜러 인스턴스를 생성하고 딜러정보를 열심히 set 해줍니다.

Dealer dealer = new Dealer();
dealer.setName("ted");
dealer.setAddress("서울시 강남구");
dealer.setId("gun0912");

이 코드를 코틀린으로 만든다면 다음과 같이 만들 수 있습니다.

val dealer = Dealer().apply {
    name = "ted"
    address = "서울시 강남구"
    id = "gun0912"
}

apply 를 사용하면 scope안의 정보가 dealer 인스턴스 자체가 되고 바로 해당 인스턴스의 정보를 할당할 수 있습니다. 

 

 

이 apply는 이런 경우에도 좋습니다.
Activity를 실행하기위해 Intent를 만들고 거기에 여러 값들을 Extra로 넘겨주는 함수를 많이 작성해 보셨을겁니다.

private Intent getTedActivityIntent() {
    Intent intent = new Intent(this, TedActivity.class);
    intent.putExtra(EXTRA_AAA, aaa);
    intent.putExtra(EXTRA_BBB, bbb);
    intent.putExtra(EXTRA_CCC, ccc);
    return intent;
}

이러한 함수도 다음과 같이 새로운 Intent를 만들고 scope안에서 바로 간결하게 작성 가능합니다.

private fun getTedActivityIntent() =
    Intent(this, TedActivity::class.java).apply {
        putExtra(EXTRA_AAA, aaa)
        putExtra(EXTRA_BBB, bbb)
        putExtra(EXTRA_CCC, ccc)
    }

 

 

run

아까 잠깐 언급되었던 run을 다음과 같이 사용할 수도 있습니다.

private fun initRecyclerView() {
    binding.recyclerView.run {
        adapter = recyclerViewAdapter
        layoutManager = GridLayoutManager(context, SPAN_COUNT)
        addItemDecoration(DividerItemDecoration(context, LinearLayout.VERTICAL))
        isNestedScrollingEnabled = false
    }
}

만약 run을 사용하지 않았다면 binding.recyclerview.xxxx,  binding.recyclerview.xxxx를 계속 타이핑해주어야 했을것입니다.
run은 apply와 사용방식이 비슷하지만 또 다릅니다.

 

 

 

apply / also / run / let / with

Apply, also ,run, let, with가 사용방식이 비슷하면서도 각자가 다른 구조를 갖고 있습니다.

https://medium.com/androiddevelopers/kotlin-standard-functions-cheat-sheet-27f032dd4326

이 자료는 구글 미디엄글에서 각각을 활용하면 좋은 예시를 보여주는 이미집니다.

 

또한 검색하시다보면 아래와 같이 정리되어 있는 이미지도 많이 만나시게 될겁니다.

scope에 this가 들어오냐 vs it이 들어오냐,

scope의 결과로 자기자신이 return되느냐 vs 마지막 줄의 결과가 return되느냐

등 서로가 비슷하지만 다른 개념들입니다.

 

저도 사용할때마다 헷갈릴때가 많아서
아예 출력해서 책상위에 두고 왔다갔다하면서 머리속에 넣어두고 있습니다.

 

Java코드와의 호환

코틀린은 자바와 잘 호환됩니다.
이와같이 코틀린에서 만든 클래스를 자바에서 문제없이 사용할 수 있습니다.

//Kotlin에서 만들고
class ForJava(var name: String, val birth: Int, var isShow: Boolean)
//Java에서 사용
String name = forJava.getName();
forJava.setName("ted");

int birth = forJava.getBirth();

boolean isShow = forJava.isShow();
forJava.setShow(true);

당연히 반대로 자바파일도 코틀린에서 사용할 수 있습니다.
물론 자바에서 Nullable, NonNull같은 어노테이션이 잘 붙어 있다면 거의 완벽하게 호환되서 사용할 수 있을것입니다.

 

또한 이런 어노테이션들을 이용해서 코틀린에서 만든 코드들을 자바에서 사용할 수 있도록 할 수 있습니다.

@JvmName, @JvmField, @JvmStatic, @JvmOverloads등의 어노테이션들과 Companion이라는 개념이 있습니다.

각 상황에서 쓰이는 예시들이 많으니 한번 사용하게 되시거나 검색해보시면 많은 자료를 얻으실 수 있을겁니다.

 

Extension function

Swift언어의 extension과 같습니다.
기존에 정의되어있는 클래스에 함수를 추가해서 원래 정의되어있는 함수인것마냥 내가 원하는대로 확장해서 사용할 수 있습니다.

fun Context.showToast(text: CharSequence, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, text, duration).show()
}

fun Context.startCallActivity(callNumber: String) {
    val intent = Intent(Intent.ACTION_DIAL)
    val uri = "tel:$callNumber"
    intent.data = Uri.parse(uri)
    startActivity(intent)
}

showToast("안녕하세요")
startCallActivity("01012345678")

물론, 이를 만들때 Extension의 파일을 어떤 패턴으로 정의할것이냐 규칙 결정 필요합니다.

 

그외

더 유용하고 좋은 키워드들을 검색해서 알아보세요
- 함수형 프로그래밍(Functional programming)
- 다양한 Collection관련 함수들(kotlin.collections):  예) listOf(1, 2, 3)
- Lazy execution(Sequences)
등등



 

코틀린 변환

처음 코틀린 코드를 만나실때 기존에 자바로 작성되어 있는 코드를 안드로이드 스튜디오를 통해서 코틀린코드로 변환하는 작업을 해봅니다.

[Code] -> [Convert Java File to Kotlin File]

물론 단순 기계적인 변환이기때문에 코틀린스럽지 않게 변환되는 문제는 있습니다.
하지만 그래도 처음부터 코딩하는것보다는 좋은 결과물을 보여줍니다.

 

Java코드 보기

반대로 만들어져있는 코틀린 코드를 자바코드로 어떻게 되어있는지 확인할 수도 있습니다.
이또한 안드로이드 스튜디오에서 제공해줍니다.

[Tools] -> [Kotlin] -> [Show Kotlin Bytecode]

 

아까 정의했던 딜러 클래스를 코틀린으로 짠뒤에 이를 자바코드로 변환해본다면 

data class Dealer(var name: String?, var address: String?)

바이트코드는 아래와 같이 나오게 됩니다.

물론 뭐라는지 알아볼 수가 없겠죠…
여기서 Decompile버튼을 누릅니다.

그럼 위와 같이 아까 정의했던 Dealer클래스가 자바로 변환되어있는걸 확인할 수 있습니다.
코틀린으로 코딩하다가 자바에서는 어떻게 동작하는지 궁금하거나 확인하면서 만들어야할 경우에 아주 유용하게 쓰입니다.

현업개발

현업 개발자분들의 가장 큰 고민은 기존에 있는 자바코드를 어떻게 코틀린으로 변환할것인가에 대한 고민일 것입니다.
날잡고 모두 코틀린으로 변환하기에는 여기저기서 빵빵 터질것 같고 일정또한 피쳐개발로 인해 충분하지 않을 수도 있습니다.
그래서 저희회사는 이와 같은 코틀린 코드 작성 규칙을 세우고 조금씩 변환해가고 있습니다.

 

현재 사내 안드로이드 팀 코틀린 코드 작성 규칙
- 새로운 코드는 코틀린으로 작성
- 피쳐개발로 인해 기존코드를 수정해야할경우, 간단한 코드면 함께 kotlin으로 변환
- 여유가 될때마다 kotlin코드로 리팩토링

 

물론 제가 개인적으로 만드는 코드나 라이브러리들은 100% 코틀린으로 작성하고 있습니다.

 

 

코틀린을 자바처럼 코딩하지 말자

제가 회사나 멘토링 기타 행사에서 개발자 면접이나 과제를 리뷰할때 코틀린으로 짠 코드를 볼때 항상 코틀린을 자바처럼 짰는지를 봅니다.
코틀린을 겉핥기로 공부한사람은 코틀린을 자바처럼 코딩합니다.

 

아래는 코틀린을 제대로 잘 활용하지 않은 사례의 코드들입니다.

val name = bid!!.dealer!!.name
val address = bid!!.dealer!!.address

val intent = Intent(this, TedActivity::class.java)
intent.putExtra(EXTRA_AAA, aaa);
intent.putExtra(EXTRA_BBB, bbb);
intent.putExtra(EXTRA_CCC, ccc);


Log.d("ted", "딜러이름: " + name + " , 딜러주소:" + address)

물론 상황에따라서 느낌표를 사용하게 되기도 하지만 대부분의 케이스에서 !!를 사용하는것은 좋지 않습니다.
또한 앞에서 언급했던 여러가지 코틀린을 사용하면 유용한 것들을 사용하지 않고 자바처럼 코딩하는 코드는 좋지 않은 코드인것 같습니다.

 

물론 일부는 IDE에서 warning으로 알려주기때문에 금방 수정할 수 있습니다.
그리고 위에서 언급드린 키워드들로 검색하고 익히신다면 더 많이 개선하시게 될것입니다.

 

내가 작성한 코드를 리뷰받는게 제일 좋습니다.
회사에서 팀원들에게 코드리뷰 받으면서 서로가 서로에게 좋은 영향을 주면 좋고, 혼자 개발한다면 스터디나 여러 네트워킹을 통해서 서로가 서로에게 도움을 주면 좋을것입니다.

 

Kotlin 꼭 해야하나요?

마지막으로 더이상 앞으로는 이 질문이 나오지 않기를 바라며, 코틀린 언어를 꼭 시작하시라고 말씀드리고 싶습니다.

 

 

이상입니다.

끝까지 읽어주셔서 감사합니다.

Comments