공부/이펙티브코틀린

아이템 43 - API 의 필수적이지 않는 부분을 확장 함수로 추출하라

띵커베르 2024. 8. 10. 11:51
728x90

클래스의 메서드를 정의할 때는 메서드를 멤버로 정의할 것인지 아니면 확장 함수로 정의할 것인지 결정해야 합니다.

 

멤버 함수로 정의한 경우

class Circle(val radius: Double) {

    // 멤버 함수로 정의된 메서드
    fun area(): Double {
        return Math.PI * radius * radius
    }
}

fun main() {
    val circle = Circle(5.0)
    println("Area: ${circle.area()}")  // 멤버 함수 호출
}

 

확장 함수로 정의한 경우

class Circle(val radius: Double)

// 확장 함수로 정의된 메서드
fun Circle.area(): Double {
    return Math.PI * radius * radius
}

fun main() {
    val circle = Circle(5.0)
    println("Area: ${circle.area()}")  // 확장 함수 호출
}

 

두가지 방법은 거의 비슷하다.
호출하는 방법도 비슷하고, 리플렉션으로 레퍼런싱하는 방법도 비슷하다.

- 레퍼런싱: 함수를 참조(레퍼런스)하는 것을 의미

 


일단 멤버와 확장의 가장 큰 차이점은 확장을 따로 가져와서 사용해야 한다는것

- 확장 함수는 기본적으로 클래스의 일부가 아니기 때문에, 필요할 때만 가져와서 사용해야 한다.

- 메서드는 클래스의 public API 만 활용한다면 어디에 위치해도 상관없다.

- 임포트해서 사용한다는 특징 덕분에 확장은 같은 타입에 같은 이름으로 여러 개 만들 수 있어, 충돌하지 않는다는 장점이 있지만, 같은 이름으로 다른 동작을 하는 확장이 있다는 것은 위험할 수 있다.

 

확장은 가상(virtual)이 아니라는 것

- 확장 함수가 클래스의 멤버 함수처럼 오버라이딩 되지 않으며, 다형성이 적용되지 않는다라는 점

- 확장 함수는 컴파일 시점에 정적으로 결정됨, 즉 확장 함수는 해당 타입에 직접 바인딩되며, 런타임 시점에 실제 객체의 타입과는 상관없이 컴파일 시점에 정의된 확장 함수가 호출됨

 

"확장 함수가 '첫 번째 아규먼트로 리시버가 들어가는 일반 함수'로 컴파일되기 때문에 발생되는 결과입니다."

- 확장 함수는 코틀린의 문법적 편의 기능이지만, 실제로 컴파일될 때는 클래스의 멤버 함수처럼 처리되지 않습니다. 대신, 확장 함수는 리시버 객체를 첫 번째 인자로 받는 일반 함수로 컴파일됩니다. 이로 인해 확장 함수는 클래스의 멤버처럼 보이지만, 실제로는 그 클래스와 무관하게 동작합니다.

// 다음과 같은 확장함수가 있다면..
fun String.isPalindrome(): Boolean {
    return this == this.reversed()
}

// 컴파일 시점에 다음과 같이 변환된다.
fun isPalindrome(receiver: String): Boolean {
    return receiver == receiver.reversed()
}

String 클래스에 정의된 멤버 함수가 아닌, String을 첫 번째 인자로 받는 일반 함수가 되는 것입니다. 
따라서 확장 함수는 클래스 내부의 상태나 메서드와 상관없이 동작할 수 있습니다. 
이것이 확장 함수가 가상 함수처럼 다형성을 지원하지 않는 이유 중 하나입니다.

 

 

 

---

"확장 함수는 클래스가 아닌 타입에 정의하는 것입니다. 그래서 nullable 또는 구체적인 제네릭 타입에도 확장 함수를 정의할 수 있습니다."

- 확장 함수는 특정 클래스에 속하지 않고 타입에 대해 정의됩니다. 이 때문에 확장 함수는 nullable 타입이나 제네릭 타입에도 적용할 수 있습니다.

// Nullable 타입에 대한 확장 함수

// String?은 nullable한 String을 의미하며, 이 함수는 null 값도 처리할 수 있습니다.
fun String?.isNullOrEmpty(): Boolean {
    return this == null || this.isEmpty()
}

// 제네릭 타입에 대한 확장 함수
fun <T> List<T>.secondOrNull(): T? {
    return if (this.size > 1) this[1] else null
}

 

 

 

 

---

마지막으로 중요한 차이점은 확장은 클래스 레퍼런스에서 멤버로 표시되지 않는다는 것입니다.
그래서 확장 함수는 어노테이션 프로세서가 따로 처리하지 않습니다.
따라서 필수적이지 않은 요소를 확장 함수로 추출하면, 어노테이션 프로세스로부터 숨겨집니다.
이는 확장 함수가 클래스 내부에 있는 것은 아니기 떄문입니다.

확장 함수는 클래스 레퍼런스에서 멤버로 표시되지 않는다
- 확장 함수는 클래스 내부에 정의되지 않으며, 실제로는 해당 클래스의 멤버가 아닙니다. 따라서 확장 함수는 클래스의 레퍼런스(Class::class.members)에서 멤버로 표시되지 않습니다. 즉, 확장 함수는 클래스의 내부에 속하지 않으므로 클래스의 멤버로 인식되지 않습니다.

어노테이션 프로세서가 확장 함수를 따로 처리하지 않는다
- 코틀린에서 어노테이션 프로세서(Annotation Processor)는 주로 클래스의 멤버를 분석하고 처리하는데 사용됩니다. 이 과정에서 어노테이션 프로세서는 클래스의 멤버(필드, 메서드 등)에 적용된 어노테이션을 읽어들이고, 그에 따라 특정 코드를 생성하거나 다른 작업을 수행합니다.
확장 함수는 클래스의 멤버가 아니기 때문에, 어노테이션 프로세서가 클래스의 멤버를 분석할 때 확장 함수를 고려하지 않습니다. 즉, 확장 함수에 붙은 어노테이션은 어노테이션 프로세서에 의해 처리되지 않습니다. 이는 확장 함수가 클래스 내부에 정의된 메서드처럼 동작하지 않기 때문입니다.

필수적이지 않은 요소를 확장 함수로 추출하면 어노테이션 프로세서로부터 숨겨진다
- 확장 함수는 클래스의 기본 멤버가 아니기 때문에, 어노테이션 프로세서에 의해 무시될 수 있습니다. 예를 들어, 특정 클래스에 대해 JSON 직렬화 어노테이션을 사용하는 라이브러리가 있다고 가정해봅시다. 이 경우 어노테이션 프로세서는 클래스의 멤버들을 분석하여 JSON 필드로 변환할 항목을 선택합니다. 그러나 확장 함수는 이 과정에서 무시되므로, 필수적이지 않은 기능을 확장 함수로 정의함으로써 어노테이션 프로세서가 이를 무시하도록 할 수 있습니다.

 

어노테이션 프로세서(Annotation Processor)는 어노테이션(annotation)을 처리하는 도구로, 주로 컴파일 타임에 특정 작업을 수행하기 위해 사용됩니다. 어노테이션 프로세서는 자바와 코틀린에서 사용될 수 있으며, 컴파일 시점에 코드를 생성하거나, 어노테이션이 적용된 코드에 대해 검증 및 변환 작업을 수행하는 역할을 합니다.

 

728x90