인프런/자바 개발자를 위한 코틀린 입

9) 코틀린에서 다양한 함수를 다루는 방법

backend dev 2025. 1. 10.

1. 확장함수

코틀린은 Java와 100% 호환하는 것을 목표로 하고 있다.

기존 Java 코드 위에 자연스럽게 코틀린 코드를 추가할 수 있고 싶다.

즉 Java로 만들어진 라이브러리를 유지보수, 확장할 때 Kotlin 코드를 덧붙일 수 있어야한다.

그래서 나온 개념이

어떤 클래스안에 있는 메소드처럼 호출할 수 있지만, 함수는 밖에 만들 수 있게 하자

즉 함수의 코드 자체는 클래스 밖에 있지만 마치 클래스 안에 있는 멤버함수처럼 호출해서 사용하는것이다.

 

 

String.kt라는 기본적으로 제공되는 이 클래스에

확장함수를 추가해보자.

 

fun String.lastChar(): Char {
    return this[this.length - 1]
}

fun main() {
    val lastChar = "ABC".lastChar()
    println(lastChar)
}

 

문자열의 마지막 문자를 반환하는 함수를 추가해보았다.

 

fun 확장하려는클래스.함수이름(파라미터): 리턴타입 {
// this를 이용해 실제 클래스 안의 값에 접근
}

확장함수 사용방법은 위와 같다.

 

 

위의 예시를 수정한 코드인다. 아래도 동작한다.

fun String.lastChar(): Char {
    return this[length - 1]
}

 

 

리시버[= 수신객체 타입]

- "확장하려는 대상이 되는 클래스나 타입"을 의미한다.

- 예를 들어 String 클래스에 새로운 기능을 추가하고 싶다면, String이 리시버가 된다.

 

확장 함수란

- 기존 클래스에 새로운 기능(메서드)을 추가하는 방법

- 원본 클래스의 코드를 수정하지 않고도 새로운 기능을 추가할 수 있다.

fun 리시버타입.함수이름(): 반환타입

 

위의 예에서 String이 리시버이고

this가 리시버 객체[=수신 객체]를 의미한다.

 

확장 함수 내부에서의 특별한 점:

  • 리시버의 모든 멤버(프로퍼티와 메서드)를 직접 사용할 수 있습니다
  • this를 생략할 수 있습니다
  • 마치 그 클래스 안에서 직접 메서드를 작성한 것처럼 동작합니다

 

 

확장함수 캡슐화?

 

 

확장함수가 public이고, 확장함수에서 수신객체클래스의 private 함수를 가져오면 캡슐화가 깨지기 때문에

 

확장함수는 클래스에 있는 private 또는 protected 멤버를 가져올 수 없다

 

[ 확장함수를 정의한곳은 다른 파일이고, 다른 파일에서 private 함수를 불러오는것은 말이 안되니까 ]

 

 

 

확장함수,멤버 함수 같은이름일때 

 

이미 존재하는 함수명으로 확장함수를 만들면

멤버함수가 우선적으로 호출된다.

 

그 반대 순서로 확장함수를 먼저 만들었고, 다른 기능의 똑같은 이름의 멤버함수가 생기면 오류가 발생할 수 있다

 

 

확장함수 오버라이드

 

결과

 

해당 변수의 현재 타입

즉, 정적인 타입에 의해 어떤 확장함수가 호출될지 결정된다.

 

2번째 같은 경우 Srt는 Train의 하위클래스이므로 Train객체 변수에 담길수 있다.

그대신 확장함수는 확장함수를 사용하려는 변수의 현재 타입을 보고 결정된다.

 

 

정리

 

1. 확장함수는 원본 클래스의 private, protected 멤버 접근이 안된다

 

2. 멤버함수, 확장함수 중 멤버함수에 우선권이 있다

 

3. 확장함수는 현재 타입을 기준으로 호출된다

 

 

 

 

 

+

 

Java에서 Kotlin 확장함수를 정적 메소드 부르는것 처럼 사용 가능하다.

PersonKt.lastChar("ABC");

파일명+Kt.확장함수명();

 

확장함수라는 개념은 확장프로퍼티와도 연결된다.

 

확장 프로퍼티

 

클래스의 프로퍼티도 확장할 수 있다.
다만, 확장 프로퍼티는 상태를 저장할 수 없기 때문에 초기화할 수 없고 get()을 구현한다.

그리고 가변적으로 변할 수 있는 클래스의 프로퍼티의 경우 get()과 set()을 추가할 수 있다.
(var로 선언해야 함)

// 확장 프로퍼티 선언
val String.lastChar: Char
  get() = get(length - 1)

// 가변적인 확장 프로퍼티의 경우 var로 선언
var StringBuilder.lastChar: Char
  get() = get(length - 1)
  set(value: Char) {
  	this.setCharAt(length - 1, value)
  }


fun main() {
  println("Charming".lastChar) // g
  
  val sb = StringBuilder("Charming?")
  sb.lastChar = '!'
  println(sb) // charming!
}

 

  • StringBuilder와 같이 내부 상태를 변경할 수 있는 클래스를 의미합니다
  • 예를 들어 String은 불변(immutable)이지만, StringBuilder는 가변(mutable)입니다
  • StringBuilder는 내부 문자열을 수정할 수 있는 메서드들(append, insert, setCharAt 등)을 제공합니다
  • String은 내부 문자열을 바꾸려면 새로운 String 객체를 생성해야한다.

확장 프로퍼티가 상태를 저장할 수 없는 이유

 

  • 확장 프로퍼티는 실제로 클래스에 새로운 필드를 추가하는 것이 아닙니다
  • 대신 getter와 setter 메서드를 통해 기존 클래스의 기능을 확장하는 것입니다
  • 예시 코드에서 String.lastChar는 실제로 새로운 변수를 만드는 것이 아니라, 마지막 문자를 가져오는 계산된 속성입니다

 

StringBuilder의 lastChar 확장 프로퍼티의 set()이 프로퍼티의 값[필드,backing field]를 변경하는거 같지만

실제로는 lastChar 확장 프로퍼티에 접근한 객체 (this)에다가 .setCharAt()를 진행하는 로직을 진행한다.

 

 

  • set()은 실제 필드를 수정하는 것이 아니라, 객체의 상태를 변경하는 로직입니다.
  • 확장 프로퍼티실제 필드가 없고, 객체에 대한 동작을 정의하는 것이기 때문에, set()은 객체의 상태를 수정하는 방법을 정의하는 것입니다.
  • 확장 프로퍼티실제 필드를 가지고 있지 않지만, 그 객체의 상태를 변경하거나, 값을 읽는 방법을 정의하는 방식으로 동작

 

 

즉 field,backing filed가 없는 확장 프로퍼티의 set()은 해당 프로퍼티에 접근한 객체의 상태를 변경시켜주는 로직이 들어있다.

 

원래는 field가없는 프로퍼티에 set()은 구현 못하게 되어있지만 확장 프로퍼티에는 허용한다.

 

확장 프로퍼티는 기존 클래스에 존재하는 필드나 프로퍼티를 변경하지 않고, 그 클래스의 동작을 확장하는 방식입니다. 확장 프로퍼티는 실제로 백킹 필드를 생성하지 않기 때문에, 그 자체로 값을 저장하지 않지만 set()을 구현할 수 있는 이유는 프로퍼티의 값을 수정하는 것이 아니라 객체의 상태를 변경하는 로직을 구현하는 형태로 동작하기 때문입니다.

 

 

  • 일반적인 프로퍼티에서는 백킹 필드가 없으면 set()을 정의할 수 없습니다. 즉, set()은 프로퍼티 값을 저장할 수 있는 공간이 있어야만 동작합니다.
  • 확장 프로퍼티는 실제 필드를 생성하지 않고 기존 객체의 상태를 변경하는 방식으로 set()을 구현할 수 있기 때문에, 백킹 필드가 없어도 set()을 구현할 수 있습니다.
  • 확장 프로퍼티는 기존 객체의 상태를 수정하는 로직을 정의하는 것이라, set()을 사용할 수 있는 특별한 예외적인 경우입니다.

 

 

 

위에서 확장 함수로 만들었던걸 확장 프로퍼티로 바꿔보자

val String.lastChar: Char
    get() = this[length-1]


fun main() {
    val lastChar = "ABC".lastChar
    println(lastChar)
}

 

확장 프로퍼티는 값[=field,backing filed]를 가질 수 없다. 

  • 확장 프로퍼티는 이미 존재하는 클래스에 새로운 프로퍼티를 추가하는 것입니다
  • 하지만 실제로 클래스 내부에 물리적인 필드를 추가하는 것은 불가능합니다
  • 따라서 확장 프로퍼티는 backing field를 가질 수 없습니다
  • 그래서 getter나 setter만 정의할 수 있습니다
  • backing field가 없으므로 초기값을 가질 수 없다
  • 대신 getter/setter를 통해 계산된 값만 제공할 수 있다

2. infix 함수 ( 중위 함수)

중위함수 = 함수를 호출하는 새로운 방법

for (i in 5 downTo 1 step 2) {
    print(i)
}

 

downTo, step 둘다 중위함수이다.

변수.함수이름(argument) // 일반함수 호출

변수 함수이름 argument // 중위함수 호출

 

예시)

fun Int.add(other: Int): Int {
    return this+other
}

infix fun Int.add2(other: Int): Int {
    return this + other
}

 

add,add2라는 확장함수를 만들었다.

 

add는 일반함수, add2는 중위함수이다.

println(3.add(5))
println(3 add2 5)

다음과 같이 각 함수의 사용방법이 다르다.

 

확장함수말고 멤버함수에도 infix를 붙일 수 있다 [ 중위함수로 만들수 있다.]

 


 

3. inline 함수

함수가 호출되는 대신, 함수를 호출한 지점에 함수 본문을 그대로 복사하는게 inline 함수이다.

 

위에서 만든 add함수를 inline함수로 바꿔보면

inline fun Int.add(other: Int): Int {
    return this+other
}

 

그리고 자바코드로 변환시켜보면

 

var0 에다가 other$vi를 직접더하는 코드가 보인다.

즉 함수 본문(로직)이 그대로 전달된 모습이다.

 

add2의 경우 코틀린에 구현된 함수를 호출하는 모습이지만

inline 함수는 함수 본문이 그대로 전달된다.

 

함수를 호출할때 발생하는 오버헤드를 줄이기 위해 사용된다고 한다.

 

inline의 이점

  • 성능 최적화: 람다 객체 생성을 방지하고 함수 호출 오버헤드를 줄임.
  • 간단한 코드 삽입: 컴파일 타임에 코드가 삽입되어 성능 향상.

inline 함수의 사용은 성능 측정과 함께 신중하게 사용되어야한다.


4. 지역함수

함수 안에 함수를 선언할 수 있는걸 지역함수라고 부른다.

 

지역함수 예시)

fun Int.add(other: Int): Int {
    fun printAdd(other: Int) {
        println(this)
        println(other)
    }

    printAdd(other)
    
    return this+other
}

 

함수로 추출하면 좋을 것 같을때

이 함수를 지금 함수 내에서만 사용하고 싶을 때

 

지역함수를 사용하면 된다.

 

하지만

 

depth가 깊어지기도 하고, 코드가 그렇게 깔끔하지는 않다

 

써볼일이 별로없다.

 

 

댓글