본문 바로가기

Kotlin

코틀린, 코틀린답게 사용하기 - 2. Extensions

자바에 익숙한 제가 코틀린을 처음 사용했을때 자바에서는 못보던 유틸성 메서드들이 코틀린에서 새롭게 추가된 것을 확인할 수 있었습니다. 처음에는 뭐 새롭게 추가됐나보다 하고 넘겼었는데 가만히 생각해보니,

코틀린은 자바와 100% 호환성을 목표로하고있다는데.. 기존 자바 API에 추가하지않고 어떻게 이런기능들을 제공하는지 궁금해졌습니다.

 

이 부분에 대해서 Kotlin in action 3장 - 함수 정의와 호출 에서 자세하게 다루고있는데, 대표적으로 자주 사용되는 함수인 joinToString() 을 구현하면서 알아보도록 하겠습니다.

 

val numbers = listOf(1, 2, 3, 4, 5, 6)
println(numbers.joinToString()) // 1, 2, 3, 4, 5, 6
println(numbers.joinToString(prefix = "[", postfix = "]")) // [1, 2, 3, 4, 5, 6]
println(numbers.joinToString(prefix = "<", postfix = ">", separator = "•")) // <1•2•3•4•5•6>

 

위에서 보는 것처럼 아무 파라미터를 받지않았을 때는 기본적으로 comma(,) 를 통해 구분되고, prefix, postfix, separator 를 지정하면 해당하는 문자를통해 listString 으로 반환되는 모습을 확인할 수 있습니다~ 

 

 

1. 네임드 파라미터 & 디폴트 파라미터 

위의 함수를 자바로 구현해볼까? 생각해보면 처음에는 쉬워보인다.
대충 for문돌면서 index와 index 사이에 separator 넣고 시작과 끝에 prefix, postfix 넣으면 되겠네 라고 생각했었는데..

 

구현하려고하는 joinToString() 메서드는 최대 3개의 파라미터를 받고있습니다. 물론 무조건 3개의 파라미터를 받아 호출하고 필요하지 않는 파라미터는 빈문자를 받아 처리하면 하나의 메서드로 구현이되겠지만, 파라미터의 순서도 알고있어야되고 상당히 귀찮아집니다..

 

이런 문제를 자바에서는 간단히 메서드 오버라이딩을 통해 구현할 수 있죠.. 그런데 간단하지가 않습니다. 파라미터가 3개일때 모든 경우의 수를 따진다면 8(=2^3)개의 메서드를 구현해야하죠. 이것도 물론 순서가 보장돼 있을때나 가능한 얘기네요..

 

1.1 Named Argument

위의 joinToString 을 호출하는 코드에도 나와있지만 코틀린에서는  (parameter = ) 를 통해 이름을 명시하여 순서에 상관없이 함수를 호출할 수 있습니다.  

numbers.joinToString(postfix = ">", separator = "•", prefix = "<")

뭐 이런식의 코드도 가능하다는 말이죠~ 

또한 Intellij 를 사용하면 함수를 호출할 때 numbers.joinToString(separator: ",") 처럼 파라미터+ 콜론 형태를 보여줘서 코드의 가독성을 더해주는데 이런 기능도 동시에 해결해주는 장점이 있습니다.

 

1.2 Default Argument

우리가 변수를 선언할 때 초기값을 설정하는 것처럼 코틀린에서는 함수를 정의할 때 디폴트값을 설정할 수 있습니다.

 

객체를 생성할 때 빌더를 이용해 생성하는 것처럼, 필요한 값들만 넣어서 함수를 호출하는게 가능해집니다! 내부적으로는 오버라이딩을 통해 구현이 되겠지만 우리가 실제로 만드는 메서드는 한개만 만들면 되겠네요.

 

여기까지 함수를 구현해보겠습니다.

fun <T> joinToString(
    collection: Collection<T>,
    separator: CharSequence = ", ",
    prefix: CharSequence = "",
    postfix: CharSequence = ""
): String {
  val result = StringBuilder(prefix)
  for( (index, element) in collection.withIndex()) {
    if (index > 0) {
      result.append(separator)
    }
    result.append(element)
  }
  result.append(postfix)
  return result.toString()
}

 

아직 Extension 의 개념을 알아보기전이므로 collection을 파라미터로 받았고, separator 의 디폴트 값을 ( comma + 공백 ) 설정, prefix, postfix 의 디폴트는 설정하지 않은 형태입니다.

 

중간에 보이는 withIndex() 라는 메서드도 이번 포스팅에서 다루고있는 확장함수로 { index, value } 형태인 IndexedValue 라는 타입으로 매핑해줍니다. 이렇게 매핑된 값을 Destructuring Declarations 를 사용하여 index, element 로 각각 받고 있는 모습이네요.

 

val numbers = listOf(1, 2, 3, 4, 5, 6)
println(joinToString(numbers))
println(joinToString(collection = numbers, prefix = "[", postfix = "]"))
println(joinToString(collection = numbers, prefix = "<", postfix = ">", separator = "•"))

 

얼추 비슷한 모습으로 완성되어가고 있습니다.

이제 Extenstion 에 대해서 알아보겠습니다.

 

2. 확장 함수

개념적으로 확장 함수는 단순합니다. 어떤 클래스의 멤버 메서드인것처럼 호출할 수 있지만 해당 클래스의 실제 멤버로 선언되지 않은 함수입니다.

 

과거의 자바에서는 유틸성 API를 제공하기위해 클래스의 알파벳 's' 를 더한 네이밍 컨벤션으로 새로운 클래스를 만들어 제공하였습니다.

CollectionCollections 가 대표적인 예가 되겠네요. 

 

이런 유틸성 클래스에 static 멤버 메서드로 정의하여 외부에서는 해당 클래스를 인스터스로 생성하지않고 Collections.xxx 형태로 바로 호출하여 사용하였죠. // Collections.sort()

 

코틀린에서는 이런 무의미한 유틸성 클래스가 필요하지 않습니다. 해당하는 함수가 기존 클래스에 포함된 것처럼 보여주기만 하면될 뿐이죠.

실제로 확장 함수는 이런 정적 메서드 호출에 대한 문법적인 편의일 뿐이입니다.

 

이런 확장 함수를 정의하는 것은 매우 간단합니다. 확장할 클래스 타입만 메서드앞에 선언해주면됩니다. 

 

fun <T> Collection<T>.joinToString(
    collection: Collection<T>,
    separator: CharSequence = ", ",
    prefix: CharSequence = "",
    postfix: CharSequence = ""
): String {
  ...
}

 

이 때 확장할 클래스 타입을 Receiver Object Type 이라고 하고, 실제로 확장 함수가 호출대는 대상이 되는 객체를 Receiver Object 라고합니다. 

 

 

  val numbers = listOf(1, 2, 3, 4, 5, 6)
  println(numbers.joinToString())
  println(numbers.joinToString(prefix = "[", postfix = "]"))
  println(numbers.joinToString(prefix = "<", postfix = ">", separator = "•"))

 

 

이제 맨처음에 보여드렸던 코드를 실행했을 때 동일한 결과를 얻을 수 있게 되었습니다.

 

마무리

당연하게도, 이렇게 외부에서 정의된 확장 함수는 기존의 클래스 내부 private, protected 멤버에 접근할 수 없습니다. 이런 이유로 확장 함수를 정의하더라도 캡슐화가 깨지지 않습니다.

 

또한, 기존 클래스 파일과 분리하여 새로운 확장 함수들을 모아 새롭게 파일을 만든다면 관리하는 측면에서 용이한 장점을 살릴 수 있겠네요.

하지만 이렇게 확장 함수를 통해 유틸성 함수를 만드는 것이 객체지향적인지는 생각해볼 문제인것같습니다. 역할에따라 클래스와 유틸클래스로 나눠져있던게 외부에서 봤을 때는 합쳐졌으니 클래스의 성격에따라 역할이 모호해질 수도 있을 것같습니다.

 

 

 

 

 

More Depth

  • 디폴트 파라미터 & 네임드 파라미터, 확장 함수 in Java
  • 확장 함수 임포트
  • 확장 함수 오버라이딩
  • 확장 프로퍼티

 

참고