Kotlin이 Java보다 매력적인 부분?

SSY·2023년 2월 8일
0

Kotlin

목록 보기
4/8
post-thumbnail

서론

"Kotlin언어가 세상에서 제일 효율적이다"

라는 말은 어쩌면 정말 위험한 말일수도 있다. 마찬가지로 "Kotlin이 Java보다 효율적이다" 라는 말도 위험한 말일수도 있다.

하지만 내가 최근에 공부하고 있는 Kotlin In Action을 보다보면 Java보다 상대적으로 좋은 점만 말해주고 있다. 그래서 그런지 확증편향적으로 지금의 나는 'Kotlin이 Java보다 좋다'라고 생각하고 있다.

뿐만 아니라, Kotlin언어는 Java기반으로 만들어진 언어이다. 기존 Java에서 불편한 점을 Wrapper Class를 통해 제공해주고 있다. 이런 편리한 점이 한둘이 아니다.

대표적인 예를 한가지 꼽아보자면, Kotlin은 배열을 마치 Collection처럼 다룰 수 있다는 것이다. 이는 상당히 매력적인 부분이다.

배열을 콜렉션 전용 함수를 쓰는 코드 예시

// 2,4,6, ... 18,20 원소 생성
val intArray = IntArray(10) { it * 2 }
intArray.filter {
    it % 4 == 0 // 4의 배수인것만 리스트에 담음
}.map {
    it * 2 // 거기에서 2를 곱함
}

여기서 실력이 있으신 분들은 다음과 같은 의문이 들 수 있다고 생각한다.

"IntArray로 생성된 각각의 숫자 원소들...Collection같이 생겨먹었는데... int의 Wrapping객체인 Integer아니야?"

하지만 그렇지 않다. 위 IntArray는 오롯이 원시타입 배열을 다룬다. 원시타입의 배열 객체만 다루는데, 저렇게 Collection전용 함수를 쓸 수 있다는 점. 바로 Kotlin의 장점이 아닐까 한다.

이제 본격적으로 필자가 생각하는 Kotlin의 매력에 대해서 서술해볼까 한다.

서두에 너무 Kotlin이 최고다 라는 식으로 말했는데... 물론 내가 Java의 진가를 알 수 있는 자료를 보다보면 나의 생각이 또 바뀔수도 있다. 하지만 안드로이드 개발자인 내가 그런 생각을 가지게 될까?

1. 안전한 Null처리

Java에서 다음과 같은 함수가 있다고 하자.

public void test(String str) {
     Log.i("javaLog", str);
 }

아주 간단한 함수다. 뭐 기능적으로 문제가 있어보이지 않는다.

하지만 위 함수에 인자로 null을 넣어준다면?

test(null);

아주 멋있게 Crash가 나는걸 볼 수 있다. 그래서 이를 다음과 같이 처리해줘야 한다.

public void test(String str) {
    if (str != null) {
        Log.i("javaLog", String.valueOf(str.length()));
    } else {
        Log.i("javaLog", "null임");
    }
}

즉, test의 파라미터는 null인지 notNull인지를 서두에 밝혀줘야 한다는 점이다. 하지만 Kotlin에서는 다르다. 파라미터 타입을 Nullable한지를 미리 적어줄 수 있다.

fun test(str: String) {
    Log.i("javaLog", str.length.toString())
}

그리고 이를 호출부에서부터 막을 수 있다.

TMI
IntelliJ와 Android Studio는 JetBrains에서 만들었다. 근데 신기한 점이 Kotlin언어도 JetBrains에서 만들었다는 점이다. 그래서 Kotlin이란 언어는 IntelliJ와 Android Studio의 Compiler와 찰떡궁합이다.

만약 Nullable한 변수를 넣어주고 싶다면 다음과 같이 함수를 바꿔주면 된다.

fun test(str: String?) {
    Log.i("javaLog", str?.length.toString())
}

바뀐 부분은 다음과 같다.

  • 파라미터 String타입 뒤에 '?'가 붙었다. 이는 Nullable하단 뜻이다.
  • str 변수 뒤에 Nullable한 표시 '?'를 붙여주었다. str이 있다면 값을 쓰고 없다면 안쓰겠단 뜻이다.

물론 Java에서도 '@NotNull', '@Nullable'어노테이션이 존재한다. 그리고 이걸 가지고 Null Check를 수행할 수 있다. 하지만 그럼에도 불구하고 Kotlin의 ' ? '연산자를 사용한 Null Check가 훨씬 간결한것은 사실이다.

2. 다양한 함수 기능

Kotlin은 Java에서는 제공해주지 못하는 여러가지 함수 기능을 제공해준다. 그 기능들을 하나하나 보자.
(extension fun, inline fun, infix fun, operator fun)

장담하건데, 이 4가지 함수의 특징을 이해하면 함수 가독성이 더 좋아질것이다.

2.1. extension fun

확장함수의 의미는 말 그대로 함수 내부에서 특정 클래스를 확장하는것을 의미한다. 그럼으로써 그 클래스의 public한 멤버들을 마음껏 가져다 쓸 수 있다. 예시로 들어보면 다음과 같다.

fun String.test() { }

Java만 쓰다 이런 함수 형태를 보면 무슨 뜻인지 도통 감이 오지 않을 수도 있다. 하지만 생각보다 아주 쉽다. String의 멤버를 { ... } 안에서 this(=묵시적 receiver)로 참조할 수 있다는 점이다. 안드로이드 스튜디오를 통해 보면 다음과 같이 컴파일러가 String 전용 프로퍼티 'length'를 추천해주는걸 볼 수 있다.

가독성을 살리기 위해 this를 안쓰고 바로 참조할 수도 있다.

여기까지 보면 궁금증이 다 안풀릴 수 있다. 중요한 점은 이 함수를 어떻게 사용하는지를 알아야 한다는 점이다. 바로 아래와 같이 사용한다.

"helloWorld".test()

// 또는

val str = "helloWorld"
str.test()

이제 로그를 찍어보기 위해 Log를 추가하여 함수를 재정의한다.

fun String.test() {
    Log.i("kotlinTest", "extensStrLength : $length")
}

콘솔에 결과를 찍어보면?

여기까지가 확장함수의 전부이다. 확장함수 자료형 'String'부분을 원하는 자료형 입맛대로 바꿀 수 있다.
(Boolean, Int, Char, Double, Float, 사용자 정의 클래스...등)

이제 extension fun이라는 재료를 +1 획득했다. 앞으로 남을 문법적 요소들을 머릿속에 넣으면 무궁무진한 조합을 낼 수 있을거라 생각한다.

2.2. inline fun

inline함수의 정의는 다음과 같다.

[inline함수 정의]
in + line 이라는 뜻 그대로, 함수 호출 라인에 해당 함수 본문을 넣어주는 함수를 의미한다.

가장 직관적인 이해를 위해 inline함수와 일반함수를 Decompile하여 생성되는 Java코드를 각각 비교해보자. 그럼 이해가 5초안에 된다.

우선 이해를 위해 새로 정의한 초 간단 클래스를 정의해본다.

class TestModule {

    fun main() {
        Log.i("TestModule", "startMain")
        subFun()
    }

    fun subFun() {
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
    }

}

이제 위 함수의 Decompile된 바이트 코드를 열어보자.

[일반 함수가 Decompile된 Java코드]

public final class TestModule {
   public final void main() {
      Log.i("TestModule", "startMain");
      this.subFun();
   }

   public final void subFun() {
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
   }
}

특별한 점은 없다. 그나마 눈여겨 볼 부분은 'this.subFun()'부분이다. main내부에서 subFun을 그대로 호출해주고 있다.

이제 위 일반함수 앞에 'inline'키워드만 붙여보자.

class TestModule {

    fun main() {
        Log.i("TestModule", "startMain")
        subFun()
    }

    inline fun subFun() {
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
        Log.i("TestModule", "startSubFun")
    }

}

Let's Decompile.

[inline 함수가 Decompile된 Java 코드]

public final class TestModule {
   public final void main() {
      Log.i("TestModule", "startMain");
      int $i$f$subFun = false;
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
   }

   public final void subFun() {
      int $i$f$subFun = 0;
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
      Log.i("TestModule", "startSubFun");
   }
}

여기서가 중요하다. main함수 내, subFun호출부분을 보자. 우선 첫 번째로 함수 호출이 사라졌다. 그리고 함수의 본문으로 대치되었다. 이게 바로 inline함수이다.

[inline함수 정의]
in + line 이라는 뜻 그대로, 함수 호출 라인에 해당 함수 본문을 넣어주는 함수를 의미한다.

그럼 이러한 inline함수를 통해 얻을 수 있는 이점이 뭘까? 결론 먼저 말하면 다음과 같다.

[inline함수의 이점]
1. 불필요한 무명 클래스 생성 방지
2. Generic타입의 RunTime 소거 예방

Tip
아래 [3. 함수 타입 지원]을 함께보면 이해가 빠를거라 생각한다.

a. 불필요한 무명 클래스 생성 방지

순서가 좀 꼬이긴 헀는데, 자세한 이해를 원하면 [3. 함수 타입 지원]을 보고오라. 아무튼. 우선, Kotlin에서 있어서 파라미터에 '함수타입'을 선언하고 호출해보자.

class TestModule {

    fun main() {
        Log.i("TestModule", "startMain")
        subFun() {
            Log.i("TestModule", "endSubFun")
        }
    }

    fun subFun(hello: () -> Unit) {
        Log.i("TestModule", "startSubFun")
        hello()
    }

}

그리고 Decompile

public final class TestModule {
   public final void main() {
      Log.i("TestModule", "startMain");
      this.subFun((Function0)null.INSTANCE);
   }

   public final void subFun(@NotNull Function0 hello) {
      Intrinsics.checkNotNullParameter(hello, "hello");
      Log.i("TestModule", "startSubFun");
   }
}

여기서 가장 주목할만한 부분은 'this.subFub(...)'내부에 무명 객체를 만들어 주었다는 점이다.

무명객체란?
invoke인터페이스 == 함수형 인터페이스 == 무명객체 모두 같은 말로써, 인터페이스 내에 단 1개의 함수만 존재하는 인터페이스를 의미한다.

만약, 우리가 이런 지식이 없는 상태에서 람다 함수를 남용한다면 위처럼 불필요한 객체 리소스가 메모리에 쌓이게 된다. 하지만 이를 방지하기 위해 inline함수를 쓴다면 어떨까?

class TestModule {

    fun main() {
        Log.i("TestModule", "startMain")
        subFun() {
            Log.i("TestModule", "endSubFun")
        }
    }
    
	// inline키워드 추가
    inline fun subFun(hello: () -> Unit) {
        Log.i("TestModule", "startSubFun")
        hello()
    }

}

그리고 Decompile

public final class TestModule {
   public final void main() {
      Log.i("TestModule", "startMain");
      int $i$f$subFun = false;
      Log.i("TestModule", "startSubFun");
      int var3 = false;
      Log.i("TestModule", "endSubFun");
   }

   public final void subFun(@NotNull Function0 hello) {
      int $i$f$subFun = 0;
      Intrinsics.checkNotNullParameter(hello, "hello");
      Log.i("TestModule", "startSubFun");
      hello.invoke();
   }
}

주목할 점은 불필요한 무명 객체가 사라졌다는 점이다. 즉, 결론은 우리가 람다함수를 쓸때 성능 최적화를 위해 inline을 사용하면 상당한 리소스를 절약할 수 있다.

그럼 Kotlin stdlib은 어떻게 개발되었는지 모범사례를 한번 봐볼까? Kotlin의 Collection함수를 파헤쳐보자.

각각의 함수들은 모두 함수 타입의 파라미터를 취하고 있다. 그리고 해당 함수들을 호출할 때, 불필요한 무명 객체를 생성시켜주지 않기 위해 inline으로 선언되어 있는걸 볼 수 있다.

b. Generic타입의 RunTime 소거 예방

Generic타입은 Java에서도 그렇지만 RunTime때 소거된다. 해당 부분에 대해 자세히 알고싶다면 다음 포스팅을 보고오면 이해가 될것이다.

우리가 Generic을 가지고 이를 RunTime에 핸들링하려 했을때, IDEA compiler는 다음과 같은 에러를 보여준다.

Generic타입을 reified로 선언하라 한다.

생각해보자
내가 간단히 만든 저 'subFun'이라는 함수. 런타임에 쓰기 위한 목적은 뭘까? 바로, Generic타입의 클래스 이름을 알아내기 위해서이다. 하지만 말했다시피, Generic은 RunTime에 소거된다 말했다. 이를 방지해주는 것이 바로 reified이다.

자세한 사항은 위에 첨부한 [다음 포스팅]을 보고오면 쉬울거라 설명은 생략한다.

inline함수를 마치며
Kotlin의 지원함수 중, inline함수가 알야야할 점이 가장 많은 함수라 생각한다. 그 중, 반드시 알아야할 부분이 따로 있다고 생각한다. 글이 너무 길어질것 같아 해당 부분은 다음 글들에서 따로 정리해 두었다.

2.3. infix fun

개인적으로 Kotlin 가독성 향상의 꽃이라 생각하는 기능이다. 이에 대한 함수를 작성한 사례가 있으니 아까 첨부했던 포스팅에서 infix함수를 눈여겨보고 오면 도움이 될것이다.

간단하게 설명할까 한다. HashMap은 누구나 알고있다시피 <key, value>들로 이루어진 Collection이다. 그리고 이걸 까보면 Pair객체로 담는다는걸 볼 수 있다.

// HashMap 호출부
HashMap<String, String>().apply {
    "key1" to "value1"
    "key2" to "value2"
    "key3" to "value3"
}

여기서 'to'를 Kotlin에서 기본 제공해주는 '연산자'정도로 생각할 수 있다. 하지만 그렇지 않다. 이는 Kotlin에서 제공해주는 infix기능을 사용한 '함수'이다.

즉, infix함수를 사용하면 아래와 같이 '의미적으로 가독성이 향상'된다.

// HashMap 호출부
HashMap<String, String>().apply {
    "key1" to "value1" // key1은 value1쪽을 가리킴
    "key2" to "value2" // key2은 value2쪽을 가리킴
    "key3" to "value3" // key3은 value3쪽을 가리킴
}

만약 infix함수가 없었다면 다음과 같이 사용해야만 한다.

HashMap<String, String>().apply {
    put("key1", "value1")
    put("key2", "value2")
    put("key3", "value3")
}

확실히 전자의 코드가 좀 더 깔끔하게 느껴진다.

2.4.operator fun

operator fun이란?
Kotlin언어에서 제공해주는 연산자 함수를 의미한다.

해당 연산자의 종류는 크게 5가지가 있다.

  • [더하기 연산자]
  • [빼기 연산자]
  • [곱하기 연산자]
  • [몫으로 나누기 연산자]
  • [나머지로 나누기 연산자]

자세한 설명은 [Kotlin In Action]을 보면 알 수 있다. 여기서는 간단하게만 알아보려 한다.

우린 원시타입 값들에 대해 연산을 수행한다. 이 연산자는 언어에서 다음과 같은 형태로 표현된다.

val a = 10
val b = 10

//더하기
Log.i("operatorResult", a + b)

//빼기
Log.i("operatorResult", a - b)

//곱하기
Log.i("operatorResult", a * b)

//나머지로 나누기
Log.i("operatorResult", a % b)

//몫으로 나누기
Log.i("operatorResult", a / b)

Java를 쓰는 사람들은 너무 당연한 연산이다. 그리고 추가적으로 String의 더하기 연산도 허용되는 것쯤은 알고 있다.

val a = "10"
val b = "10"

//몫으로 나누기
Log.i("operatorResult", a + b)

//결과 : "1010"

하지만 만약, 이러한 연산이 '원시타입'뿐만 아니라 '참조타입'에도 적용할 수 있다면?

Let's Do It

val list = arrayListOf<String>()
list += "hello1"
list += "hello2"

그리고 신기하게도 위 += 연산자 위에 마우스를 가져다 대면 클릭할 수 있게 바뀐다. 그리고 타고 들어가면 += 연산자가 operator fun으로 재정의된 것을 볼 수 있다.

그리고 해당 함수들이 '오버로딩'된 것을 볼 수 있다. 심심할때 한번 쭉 보면 나중에 코드를 깔끔하게 만드는데 도움이 될수 있다.

같은 원리로 마이너스 연산자도 볼 수 있다.

val list = arrayListOf<String>()
list -= "hello1"
list -= "hello2"

그럼 곱셈과 나눗셈은 없을까? IntelliJ에 코드를 적어보면 Compiler가 빨간색으로 우리에게 알려주고 있다. 해당 operator fun은 정의되지 않았다고.

그러면 원시타입으로 한번 본다면? 당연히 있다.

타고 들어가보자.


'times'라는 이름으로 operator fun이 정의된걸 볼 수 있다. 다른 연산자도 모두 마찬가지다.

그럼 이들을 응용하여 우리가 operator fun을 정의해 사용해보자

class TestModule {

    fun main() {
        var point = Point(100, 100)
        
        point += (10 to 10)
        Log.i("result", "[${point.x}, x : ${point.y}]")
        
        point -= (10 to 10)
        Log.i("result", "[${point.x}, x : ${point.y}]")
        
        point *= (10 to 10)
        Log.i("result", "[${point.x}, x : ${point.y}]")
        
        point /= (10 to 10)
        Log.i("result", "[${point.x}, x : ${point.y}]")
        
        point %= (9 to 9)
        Log.i("result", "[${point.x}, x : ${point.y}]")
    }

}


class Point(
    val x: Int,
    val y: Int
) {

    operator fun plus(axios: Pair<Int, Int>): Point {
        return Point(x + axios.first, y + axios.second)
    }
    
    operator fun minus(axios: Pair<Int, Int>): Point {
        return Point(x - axios.first, y - axios.second)
    }
    
    operator fun times(axios: Pair<Int, Int>): Point {
        return Point(x * axios.first, y * axios.second)
    }
    
    operator fun div(axios: Pair<Int, Int>): Point {
        return Point(x / axios.first, y / axios.second)
    }
    
    operator fun rem(axios: Pair<Int, Int>): Point {
        return Point(x % axios.first, y % axios.second)
    }
}

Let's Build

3. 함수 타입 지원

우리가 Java에서 알고 있는 자료형은 크게 2가지 범주로 나뉜다. 하나는 원시 타입, 또 하나는 참조 타입. 원시타입이라 하면 Int, Char, Boolean같은 자료형이다. 참조 타입이라하면 개발자가 만든 클래스를 통해 만들어진 자료형을 의미한다.

하지만 Kotlin에서는 더 나아가 함수 타입을 지원한다. 그리고 그 함수는 바로 'Java8에서 익히 쓰이는 람다'에서 더 확장된 개념이다. 그리고 이러한 람다를 변수 타입으로 지정하는것까지 가능하다.

강조
[2. 다양한 함수 기능][2. 함수 타입 지원]요소들을 적절히 섞어 활용하면 불필요한 인터페이스 생성 없이 추상화 수준을 더욱 높일 수 있다. 뿐만 아니라 소스 코드의 가독성까지 더욱 높여줄 수 있다.

이제부터 설명할 함수 타입은 아래의 한 문장으로 모두 설명할 수 있다.

수신 객체 지정 람다

수신객체 지정 람다가 무엇인지 알기 전, Java만 사용했을 때 불편한 점을 알아보자. 우선, Java에서 람다를 쓰는 예시를 들어보자. 첫 걸음으로 인터페이스를 추가로 만들어 보자. 아래와 같이 말이다.

public interface JavaCallBack {
    public void onResult();
}

이제 이 CallBack인터페이스를 이용해서 두 모듈(JavaTestActivity2 + JavaTestViewModel2)을 통신하려 한다. 다음과 같이 말이다.

public class JavaTestActivity2 extends AppCompatActivity implements JavaCallBack {

    @Override
	public void onResult() { 
    	Log.i("javaLog", "in Activity"); 
    }
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_java_test);

        JavaTestViewModel2 viewModel = new JavaTestViewModel2(this);
		viewModel.getText();
    }
}

class JavaTestViewModel2 {

    JavaCallBack callBack;
    
    public JavaTestViewModel2(JavaCallBack callBack) {
        this.callBack = callBack;
    }

    public void getResult() {
        callBack.onResult();
    }
}

위 로직의 흐름 대해 먼저 간단히 설명해 보겠다.

  1. JavaCallBack 오버라이딩
  2. 'JavaTestViewModel2'객체 생성과 동시에 인자에 'JavaCallBack'객체 삽입
  3. JavaTestViewModel2.getTest()호출
  4. Activity에 정의된 'onResult호출'

람다를 통해 두 모듈 소통의 방법으로 위의 방법이 대표적이다. 이제 개선된 코틀린 코드를 보자.

class TestActivity : AppCompatActivity() {

    val callBack: () -> Unit = { Log.i("kotlinLog", "in Activity") }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)

        val viewModel = KotlinTestViewModel()
        viewModel.getText(callBack)
    }
}

class KotlinTestViewModel {
    
    fun getText(callBack: () -> Unit) {
        callBack()
    }
}

보기만해도 직관적으로 알 수 있다. 짧아졌다란 것을. 그리고 여기서 큰 변화가 하나 있다. 바로,

인터페이스 상속이 없어졌다는 점

이다. 이는 어마어마한 차이이다. 프로젝트가 커질수록 불필요한 CallBack인터페이스를 정의하는 일은 여간 성가신 일이 아니다. 더 나아가 패키지 관리도 어떻게 해야할지 고민까지 해야하니... 불편한 점이 한둘이 아니다.

위 코드에서 주목해야하는 코드는 함수형으로 선언된 변수이다.

val callBack: () -> Unit = { Log.i("kotlinLog", "in Activity")

함수자료형에 대해 간단히 설명해 보겠다. 우선 Kotlin의 자료형 지정 연산자인 ' : '옆에 괄호가 쳐져 있다. 이 괄호는 함수의 파라미터를 의미한다.

그리고 반환 지정 연산자인 ' -> '옆에 Unit은 반환형을 의미한다.(Kotlin에서 Unit은 Java의 void라 생각하면 된다.)

이제 위에서 설명했던 '확장함수'의 개념을 더해보자. 그럼 다음과 같은 변형도 가능하다.

// 위 코드의 컴파일 여부는 신경쓰지 않는다.
val callBack: String.() -> Unit = { ... }

확장함수에 파라미터들도 넣어줄 수 있다.

val callBack: (str1: String, str2: String) -> Unit = { str1, str2 ->  }

그럼 조금 더 응용해볼 수도 있다. 함수형 타입 자료형에다 확장함수 자료형을 선언해줄 수도 있다.

val callBack: String.(str1: String, str2: String) -> Unit = { str1, str2 ->  }

여기까지 왔으면 눈치챘을 것이다. 함수를 타입으로 활용함으로써 만들 수 있는 경우가 아주 많다는 것을 말이다.

val callBack: (() -> Unit).(str1: String, str2: String) -> Unit = { str1, str2 ->  }

이런 방식으로 무궁무진하게 '함수 타입'을 활용하고 적절히 조합해줄 수 있다. 그리고 이러한 점을 활용하여 불필요한 인터페이스를 없앨 수 있다.

4. 마치며

Kotlin In Action을 꼭 읽어보길 권한다. 이 책은 Kotlin Compiler개발자가 쓴 책이다. 그 어떤 책보다 Kotlin의 특성을 잘 나타나는 책이라 생각한다. 그리고 읽어본 결과 상당히 얻어간게 많다.

이 포스팅으로 Kotlin In Action의 모든 내용을 담을 순 없다. 아주 방대하기에. 하지만 이 글을 여기까지 읽은 사람들은 위 내용만 알아도 Kotlin의 상당 부분 기능을 얻어간거라 생각한다.

ps. 100% 올바른 정보를 제공하기 위해 검수하여 글을 작성했습니다. 혹시 틀린게 있다면 댓글주시면 감사하겠습니다.

profile
불가능보다 가능함에 몰입할 수 있는 개발자가 되기 위해 노력합니다.

0개의 댓글