클래스의 메서드를 정의 할 때, 메서드를 아래 중 어떤 것으로 정의할 것인지 결정해야 한다.
- 멤버로 정의할 것인가?
- 확장 함수로 정의할 것인가?
1) 멤버 함수 (Member Function)
- 클래스 내부에서 직접 정의된 메서드
- 해당 클래스에 종속됨
- 클래스 인스턴스를 통해 호출됨
class Workshop(/*...*/) {
// ...
fun makeEvent(data: DateTime): Event = // ...
val permalink
get() = "/workshop/$name"
}
2) 확장 함수 (Extension Function)
- 기존 클래스의 기능을 확장하는 함수
- 클래스 외부에서 정의됨
- 클래스 내부 멤버처럼 사용할 수 있지만, 실제로는 첫 번째 매개변수로 리시버 객체를 받는 일반 함수로 컴파일됨
class Workshop(/*...*/) {
// ...
}
fun Workshop.makeEvent(data: DateTime): Event = // ...
val Workshop.permalink
get() = "/workshop/$name"
위의 두 방법은 거의 비슷하고, 호출 방법과 리플렉션으로 레퍼런싱하는 방법도 유사하다.
두 방식의 차이점을 이야기하기에 앞서 두 방식 중 어떤 방식이 우월하다고 할 수 없다.
각각의 장단점이 있고, 상황에 맞게 잘 사용해야 한다.
따라서 무조건 적용하지 말고, 반드시 검토 후 필요한 경우에만 사용해야한다.
멤버 함수와 확장 함수의 차이점은 세가지가 있다.
1) 확장 함수는 따로 가져와서 사용해야함
2) 확장 함수는 가상(virtual)이 아님
3) 확장 함수는 클래스 레퍼런스에서 멤버로 표시되지 않음
1) 확장 함수는 따로 가져와서 사용해야함
확장 함수는 일반적으로 다른 패키지에 위치 한다.
확장 함수가 사용되는 경우
- 직접 멤버를 추가할 수 없는 경우, 데이터와 행위를 분리하도록 설계된 프로젝트
확장이 사용되는 경우의 적절한 위치
- 필드가 있는 프로퍼티는 클래스에 있어야 하지만, 메서드는 클래스의 public API만 활용하면 위치는 상관 없음
import 해서 사용되기 때문에 같은 타입에 같은 이름으로 여러개 만들 수 있다.
장점
- 여러 라이브러리에서 여러 메서드를 받을 수도 있고, 충돌이 발생하지 않음
단점
- 같은 이름으로 동작하는 다른 확장이 있다는 것은 위험
2) 확장 함수는 가상(virtual)이 아님
확장 함수는 컴파일 시점에 정적으로 선택된다. 따라서 파생 클래스에서 오버라이드 할 수 없다.
확장 함수는 가상 멤버 함수와 다르게 동작하며, 상속을 목적으로 설계된 요소는 확장 함수로 만들면 안된다.
open class C
class D: C()
fun C.foo() = "c"
fun D.foo() = "d"
fun main() {
val d = D()
print(d.foo()) // d
val c: C = d
print(c.foo()) // c -> c 타입으로 취급
print(D().foo()) // d
print((D() as C).foo()) // c -> 타입 변환 시 c의 확장 함수가 호출됨
}
위와 같은 차이는 확장 함수가 '첫 번째 아규먼트로 리시버가 들어가는 일반 함수'로 컴파일되기 때문이다.
fun foo('this$receiver': C) = "c"
fun foo('this$receiver': D) = "d"
fun main() {
val d = D()
print(foo(d)) // d
val c: C = d
println(foo(c)) // c
print(foo(D())) // d
print(foo(D() as C)) // c
}
추가로 확장 함수는 클래스가 아닌 타입에 정의하는 것이다.
그래서 아래와 같이 nullable 또는 구체적인 제네릭 타입에도 확장 함수를 정의할 수 있다.
inline fun CharSequence?.isNullOrBlank(): Boolean {
contract {
returns(false) implies (this@isNullOrBlank != null)
}
return this == null || this.isBlank()
}
public fun Iterable<Int>.sum(): Int {
val sum: Int = 0
for (element in this) {
sum += element
}
return sum
}
3) 확장 함수는 클래스 레퍼런스에서 멤버로 표시되지 않음
확장 함수는 어노테이션 프로레서가 따로 처리하지 않는다.
따라서 필수적이지 않은 요소를 확장 함수로 추출하면, 어노테이션 프로세스로부터 숨겨진다.
그 이유는 확장 함수가 클래스 내부에 있는게 아니기 때문이다.
확장 함수를 사용해야 하는 경우
1. 기존 클래스를 변경할 수 없는 경우
- 외부 라이브러리 클래스 확장
2. 선택적인 기능을 추가할 경우
- API의 필수적인 부분이 아니라면 확장 함수로 빼서 관리 가능
3. 다양한 타입에 동일한 기능을 추가할 때
- List<Int>, Set<Int> 등 여러 컬렉션 타입에 sum()을 추가하는 경우
4. nullable 타입을 다루거나 특정 제네릭 타입에 대한 처리가 필요할 때
멤버 함수를 사용해야 하는 경우
1. 클래스 내부의 필드 접근이 필요한 경우
- private 필드 또는 protected 멤버를 접근해야 하는 경우
2. 다형성이 필요한 경우
- open fun을 사용하여 상속받는 클래스에서 override하도록 설계할 때
3. 클래스의 주요 API로 제공될 경우
- Reflection을 사용하거나, 어노테이션 기반 처리가 필요한 경우
'Study OR Book > Book' 카테고리의 다른 글
[이펙티브 코틀린] 아이템40: equals의 규약을 지켜라 (0) | 2025.02.10 |
---|---|
[이펙티브 코틀린] 아이템32: 추상화 규약을 지켜라 (0) | 2025.02.03 |
[이펙티브 코틀린] 아이템29: 외부 API를 랩(wrap)해서 사용하라 (0) | 2025.01.21 |
[이펙티브 코틀린] 아이템19: knowledge를 반복하여 사용하지 말라 (0) | 2025.01.15 |
[이펙티브 코틀린] 아이템17: 이름 있는 아규먼트를 사용하라 (0) | 2025.01.08 |