HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🤩
개발
/Kotlin/
코틀린에서 구현하는 유창성

코틀린에서 구현하는 유창성

연산자 오버로딩확장 함수와 속성을 이용한 인젝팅확장 함수를 이용한 메소드 인젝팅확장 속성을 이용한 속성 인젝팅서드파티 클래스 인젝팅Static 메서드 인젝팅클래스 내부에서 인젝팅infix 를 이용한 중위표기법Any 객체를 이용한 자연스러운 코드Scope Functions함수 선택Context object : this or itapply를 이용한 반복 참조 제거run 을 이용한 결과 얻기let을 이용해 객체를 아규먼트로 넘기기also를 사용한 void 함수 체이닝암시적 리시버리시버 전달리시버를 이용한 멀티플 스코프

연산자 오버로딩

operator fun Pair<Int, Int>.plus(other: Pair<Int, Int>) = Pair(first + other.first, second + other.second)
data class Complex(val real:Int, val imaginary: Int) { operator fun times(other: Complex) = Complex(real * other.real - imaginary * other.imaginary, real * other.imaginary + imaginary * other.real) private fun sign() = if (imaginary < 0 ) "-" else "+" override fun toString() = "$real ${sign()} ${abs(imaginary)}i" }
직접 만든 클래스에서 연산자 오버로딩하기 위해, 적절히 특화된 메소드를 클래스의 멤버함수로 작성
추천사항
  • 절제하여 사용하라
  • 코드를 읽는 사람 입장에서 당연하게 받아들여질 경우에만 사용하라
  • 오버로딩된 연산자는 일반적인 연산자의 동작이어야 한다.
  • 변수이름을 의미있게 만들어라. 그래야 오버로딩의 문맥을 파악하기 좋다

확장 함수와 속성을 이용한 인젝팅

[ Kotlin docs ] Extensions
  • 상속이 불가능한 클래스 역시 확장에는 열려있다.
  • 클래스에 이미 존재하는 메소드를 확장 함수로 만들면 안된다. 충돌이 있는 경우에 클래스의 멤버 함수가 항상 확장 함수를 이긴다.

확장 함수를 이용한 메소드 인젝팅

data class Point(val x: Int, val y: Int) data class Circle(val cx: Int, val cy: Int, val radius: Int) fun Circle.contains(point: Point) = (point.x - cx) * (point.x - cx) + (point.y - cy) * (point.y - cy) < radius * radius val circle = Circle(100, 100, 25) val point1 = Point(110, 110) val point2 = Point(10, 100) println(circle.contains(point1)) println(circle.contains(point2))
  • Circle 클래스의 인스턴스에서 해당 메서드를 호출할 수 있다.
  • 코틀린의 확장 함수는 패키지의 static 메서드로 만들어진다. 그리고 컨텍스트 객체(예제에서 Circle)를 함수의 첫번째 파라미터로 전달하고, 이어서 실제 파라미터를 전달함
  • 확장 함수를 사용할 때 메서드 호출로 보이는 과정은 사실은 static 메서드를 호출하는 과정과 동일함
  • 확장함수의 한계
    • 인스턴스 메서드와 같은 이름을 갖고 있으면 항상 인스턴스 메서드가 실행됨
    • 인스턴스의 캡슐화된 부분(private 메서드 혹은 속성)에 접근할 수 있는 인스턴스 메서드와 달리, 확장 함수는 정의된 패키지 안에서 객체에 보이는 부분(public 메서드 혹은 속성)에만 접근 가능함

확장 속성을 이용한 속성 인젝팅

val Circle.area : Double get() = kotlin.math.PI * radius * radius val circle = Circle(100, 100, 25) println("Area is ${circle.area}")
  • 확장 속성은 클래스 내부에 존재하는 것이 아니기 때문에 백킹 필드를 가질 수 없다. 즉, 확장 속성은 field에 접근할 수 없다는 이야기
  • 확장 속성은 클래스의 다른 속성이나 메서드를 이용해 작업을 완료할 수 있다.

서드파티 클래스 인젝팅

fun String.isPalindrome(): Boolean { return reversed() == this }
  • 우리는 확장 함수를 서드파티 클래스에 추가할 수도 있고, 이미 존재하는 메서드로 확장 함수를 라우팅할 수도 있다.

Static 메서드 인젝팅

fun String.Companion.toUrl(link: String) = java.net.URL(link) val url: java.net.URL = String.toUrl("https://pragprog.com")

클래스 내부에서 인젝팅

class Point(x: Int, y: Int) { private val pair = Pair(x, y) private val firstsign = if (pair.first < 0) "" else "+" private val secondsign = if (pair.second < 0) "" else "+" override fun toString() = pair.point2String() fun Pair<Int, Int>.point2String() = "(${firstsign}${first}, ${this@Point.secondsign}${this.second})" }
  • 확장 함수가 클래스 내부에서 생성되었기 때문에 확장 함수에는 this와 this@Point 두개의 리시버를 갖고 있음
    • extension receiver : 확장 함수가 실행되는 객체 (위 예시에서 Pair). 즉, 확장 함수를 리시브 하는 객체
    • dispatch receiver : 확장 함수를 만들어 추가한 클래스의 인스턴스( 위 예시에서 Point). 즉, 메서드 인젝션이 된 클래스
  • 프로퍼티와 메서드 바인딩을 할 때 extension receiver가 우선순위를 가짐
  • dispatch receiver를 바로 참조하고 싶다면 this@Outer (위 예시에서 this@Point) 문법을 사용하면 됨

infix 를 이용한 중위표기법

//Java if(obj instanceof String) {
  • 위의 코드를 if(obj.instanceOf(String) { 이라고 작성해야 한다고 상상하면 꽤나 복잡하다
  • 이렇게 연산자가 중간에 있거나 피연산자 사이에 있는 것을 중위표기법(infix notation)이라 부름
  • Java에서는 이런 표현 방법이 이미 정의된 연산자에서만 제한적으로 사용할 수 있다. 그러나 코틀린에서는 코드에 중위표기법을 사용할 수 있다.
  • 코틀린에서 연산자는 항상 자동으로 중위표기법을 사용한다.
operator infix fun Circle.contains(point: Point) = (point.x - cx) * (point.x - cx) + (point.y - cy ) * (point.y - cy ) < radius * radius println(circle.contains(point1)) // true println(circle contains point1)
  • method에 infix 어노테이션을 사용하면 코틀린은 점과 괄호를 제거하는 것을 허용해준다.
  • 코틀린은 infix를 이용해 함수에 유연성을 제공함
  • 하지만 한계 역시 존재
    • infix 메소드는 정확히 하나의 파라미터만 받아야 함
    • vararg도 사용할 수 없고, 기본 파라미터도 사용 불가

Any 객체를 이용한 자연스러운 코드

[ Kotlin docs ] Scope Functions
💡
비록 scope function이 코드를 간결하게 만들어 줄 지라도, 너무 남용하지는 말라 이것은 코드를 읽기 어렵게 만들고 에러를 유발할 수 있다.
scope function을 중첩해서 사용하는 것도 피하는 것을 추천하고, 체이닝하여 사용할 때는 context object가 this인지 it 인지 헷갈릴 수 있기에 주의해서 사용해야 한다.

Scope Functions

  • 기본적으로 이 함수들은 동일한 작업을 수행함 ( object에 대해 코드 블럭을 실행)
  • 차이점은 코드블럭 내부에서 object 가 어떻게 이용가능해지는지, 모든 표현식의 결과가 무엇인지 가 다른점임

함수 선택

Function
Object Reference
Return Value
Is extension function
let
it
Lambda result
Yes
run
this
Lambda result
Yes
run
-
Lambda result
No: called without the context object
with
this
Lambda result
No: takes the context object as an argument
apply
this
Context object
Yes
also
it
Context object
Yes
  • Executing a lambda on non-nullable objects: let
  • Introducing an expression as a variable in local scope: let
  • Object configuration: apply
  • Object configuration and computing the result: run
  • Running statements where an expression is required: non-extension run
  • Additional effects: also
  • Grouping function calls on an object: with
 
  • let() 과 run(), with()은 람다를 실행시키고 람다의 결과를 호출한 곳으로 리턴
  • also() 와 apply() 는 람다의 결과를 무시하고 컨텍스트 객체를 호출한 곳으로 리턴

Context object : this or it

각각의 scope 함수는 context object를 참조하는 두가지 방법 중 하나를 사용함 (this or it)
fun main() { val str = "Hello" // this str.run { println("The string's length: $length") //println("The string's length: ${this.length}") // does the same } // it str.let { println("The string's length is ${it.length}") } }
run, with, apply 는 context object를 lambda receiver 로써 참조함 (to denote current receiver, use this keyword)
  • receiver. 수신자. 어떠한 멤버 함수를 호출하는 주체가 되는 애를 receiver 라고 하는 듯함 [참고 Function literals with receiver ]
  • lambda receiver라 하면 lambda 내부에서 함수를 호출할때의 주체인데(this 를 통해 참조) 그것이 context object 인것
class HTML { fun body() { ... } } fun html(init: HTML.() -> Unit): HTML { val html = HTML() // create the receiver object html.init() // pass the receiver object to the lambda (제공된 lambda에 html을 넘김) return html } html { // lambda with receiver begins here body() // calling a method on the receiver object }
  • let 과 also 는 context object를 lambda argument 를 통해 참조함
    • context object를 function call 의 argument로 쓸 때는 it 으로 쓰는게 더 나음
 
 
Method
Argument
Receiver
Return
Result
let
context
lexical
RESULT
RESULT
also
context
lexical
RESULT
context
run
N/A
context
RESULT
RESULT
apply
N/A
context
RESULT
context
class Mailer { val details = StringBuilder() fun from(addr: String) = details.append("from $addr ... \n") fun to(addr:String) = details.append("to $addr ... \n") fun subject(line:String) = details.append("subject $line...\n") fun body(message: String)= details.append("body $message...\n") fun send() = "...sending...\n$details" } val mailer = Mailer() mailer.from("builder@agiledeveloper.com") mailer.to("venkats@agiledeveloper.com") mailer.subject("your code sucks") mailer.body("...details...") val result = mailer.send() println(result)

apply를 이용한 반복 참조 제거

val mailer = Mailer() .apply { from("builder@agiledeveloper.com") } .apply { to("venkats@agiledeveloper.com") } .apply { subject("your code sucks") } .apply { body("...details...") } val result = mailer.send() println(result)
  • apply() 호출은 Mailer의 인스턴스에서 호출하고 같은 인스턴스를 리턴함 → 체인을 사용할 수 있도록 해줌
  • apply() 메서드는 apply() 를 마지막으로 호출한 객체의 컨텍스트에서 람다를 실행시킴. 그래서 우리는 apply()에 전달하는 람다에서 Mailer에 여러 번의 메서드 호출을 사용할 수 있음
    • val mailer = Mailer().apply { from("builder@agiledeveloper.com") to("venkats@agiledeveloper.com") subject("your code sucks") body("...details...") } val result = mailer.send() println(result)

run 을 이용한 결과 얻기

val result = Mailer().run { from("builder@agiledeveloper.com") to("venkats@agiledeveloper.com") subject("your code sucks") body("...details...") send() } println(result)
  • run() 호출은 Mailer의 인스턴스에서 호출, 람다의 결과를 리턴
  • send() 메서드의 결과가 return 되기에 이후에 Mailer 인스턴스 참조로 메서드 호출이 불가함

let을 이용해 객체를 아규먼트로 넘기기

  • let() 호출은 argument가 caller가 넘어오고, Receiver는 lexical 임. 람다의 결과를 리턴
  • let() 에 아규먼트로 전달한 람다의 결과를 사용하길 원한다면 let() 이 좋은 선택이다. 하지만 let() 을 호출한 타깃 객체에서 뭔가 작업을 계속 하길 원한다면 also()를 사용해야 한다.
fun createMailer() = Mailer() fun prepareAndSend(mailer: Mailer) = mailer.run { from("builder@agiledeveloper.com") to("venkats@agiledeveloper.com") subject("your code sucks") body("...details...") send() }
val mailer = createMailer() val result = prepareAndSend(mailer) println(result) // 첫번째 변경 val result = prepareAndSend(createMailer()) // let() 사용 val result = createMailer().let { mailer -> prepareAndSend(mailer) } // 암시적 파라미터 it 사용 val result = createMailer().let { prepareAndSend(it) } // 메서드 참조 사용 val result = createMailer().let(::prepareAndSend)

also를 사용한 void 함수 체이닝

  • also() 메서드는 체이닝을 사용할 수 없는 void 함수를 체이닝 하려고 할 때 유용하다
fun prepareMailer(mailer: Mailer): Unit { mailer.run { from("builder@agiledeveloper.com") to("venkats@agiledeveloper.com") subject("your code sucks") body("...details...") } } fun sendMailer(mailer: Mailer): Unit { mailer.send() println("Mail sent") } val mailer = createMailer() prepareMailer(mailer) sendMailer(mailer) // also()를 활용해 메서드 체이닝 적용 createMailer() .also(::prepareMailer) .also(::sendMailer)

암시적 리시버

리시버 전달

var length = 100 val printIt: (Int) -> Unit = { n: Int -> println("n is $n, length is $length") } printIt(6) // n is 6, length is 100
  • 람다 내부 스코프에 length가 없으니 lexical scoped에서 length를 찾음
var length = 100 val printIt: String.(Int) -> Unit = { n: Int -> println("n is $n, length is $length") } printIt("Hello", 6) // n is 6, length is 5
  • 람다의 시그니처 정의에서 (Int) → Unit 대신 String.(Int) → Unit 을 사용함으로써 receiver를 String 타입으로 한정할 수 있음
  • 호출 시, 추가적인 아규먼트를 전달해주어야 함. 내부에서 this 에 바운딩 될 컨텍스트 또는 리시버가 필요
  • 람다를 리시버의 멤버함수처럼 사용할 수 있음 "Hello".printIt(6)
    • 람다는 리시버의 확장 함수처럼 동작한다. 사실 이 말이 작동 방식을 가장 완벽하게 표현함

리시버를 이용한 멀티플 스코프

람다 표현식은 다른 람다 표현식에 중첩될 수 있다. 이때 내부의 람다 표현식은 멀티플 리시버를 가진 것처럼 보일 수 있다.
중첩 클래스와 이너클래스에서 봤던 이너 클래스의 리시버와도 유사함
fun top(func: String.() -> Unit) = "hello".func() fun nested(func: Int.() -> Unit) = (-2).func() top { println("In outer lambda $this and $length") nested { println("in inner lambda $this and ${toDouble()}") println("from inner through receiver of outer : ${length}") println("from inner to outer receiver ${this@top}") } } /* In outer lambda hello and 5 in inner lambda -2 and -2.0 from inner through receiver of outer : 5 nested에 length 가 없으므로 위의 스코프로 넘어감 from inner to outer receiver hello */