[Kotlin in Action] 7장 연산자 오버로딩과 기타 관례

Sdoubleu·2023년 3월 29일
0

Kotlin in Action

목록 보기
6/9
post-thumbnail

7장에서 다루는 내용

  1. 연산자 오버로딩
  2. 관례: 여러 연산을 지원하기 위해 특별한 이름이 붙은 메소드
  3. 위임 프로퍼티
  • 관례
    어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법

  • 기존 자바 클래스가 구현하는 인터페이스는 이미 고정돼 있고 코틀린 쪽에서 자바 클래스가 새로운 인터페이스를 만들 수는 없다

  • 확장 함수를 사용하면 기존 클래스에 새로운 메소드를 추가할 수 있다
    -> 기존 자바 클래스에 대해 확장 함수를 구현하면서 관례에 따라 이름을 붙이면 기존 자바 코드를 바꾸지 않아도 새로운 기능을 쉽게 부여할 수 있다


7.1 산술 연술자 오버로딩

7.1.1 이항 산술 연산 오버로딩

/* plus 연산자 구현하기 */
data class Point(val x: Int, val y: Int) {
   operator fun plus(other: Point): Point {  //"plus" 라는 연산자 함수를 정의합니다
      return Point(x + other.x, y + other.y)
   }
}

>>> val point1 = Point(10, 20)
>>> val point2 = Point(30, 40)
>>> println(point1 + point2)  //"+"로 계산하면 "plus"함수가 호출됩니다

Point(x=40, y=60)
  • 연산자를 오버로딩 하는 함수 앞에는 꼭 operator 키워드를 붙임으로써 어떤 함수가 관례를 따르는 함수임을 명확히 할 수 있다

// 연산자를 확장 함수로 정의하기
operator fun Point.plus(other: Point) : Point {
   return Point(x + other.x, y + other.y)
}
  • 외부 함수의 클래스에 대한 연산자를 정의할 때는 관례를 따르는 이름의 확장 함수로 구현하는게 일반적인 패턴이다

  • 오버로딩 가능한 이항 산술 연산자

함수 이름
a * btimes
a / bdiv
a % bmod(1.1부터 rem)
a + bplus
a - bminus
  • 직접 정의한 함수를 통해 구현하더라도 연산자 우선순위는 언제나 표준 숫자 타입에 대한 연산자 우선순위와 같다
    ex) a + b c -> 는 + 보다 먼저 수행된다

  • 연산자를 정의할 때 두 피연산자가 같은 타입일 필요는 없다

// 두 피연산자의 타입이 다른 연산자 정의하기
operator fun Point.times(scale: Double) : Point {
   return Point( (x * scale).toInt(), (y * scale).toInt() )
}

>>> val point = Point(10, 20)
>>> println(point * 1.5)

Point(x=15, y=30)


// 1.5 * p , p * 1.5 둘 다 지원하려면
operator fun Double.times(p: Point): Point를 정의해야 한다


// 결과 타입이 피연산자 타입과 다른 연산자 정의하기
operator fun Char.times(count: Int) : String {
   return this.toString().repeat(count)
}

>>> println('A' * 3)

AAA

비트 연산자에 대해 특별한 연산자 함수를 사용하지 않는다
코틀린은 표준 숫자 타입에 대해 비트 연산자를 정의하지 않는다
커스텀 타입에서 비트 연산자를 정의할 수 없다

7.1.2 복합 대입 연산자 오버로딩

  • +=, -= 등의 연산자는 복합 대입 연산자라고 불린다
>>> var point = Point(1, 2)
>>> point += Point(3, 4)  // point = point + Point(3, 4)와 동일
>>> println(point)

Point(x=4, y=6)

// 객체에 대한 참조를 다른 참조로 바꾸기보다
// 원래 객체의 내부 상태를 변경하게 만들 때도 사용 가능
>>> val numbers = ArrayList<Int>()
>>> numbers += 42
>>> println(numbers[0])

42

  • plus와 plusAssign 연산을 동시에 정의하지 말자

    • 와 - 는 항상 새로운 컬렉션을 반환
      += 와 -+ 연산자는 항상 변경 가능한 컬렉션에 작용해 메모리에 있는 객체 상태를 변화시킨다
  • 읽기 전용 컬렉션에서 += 와 -=는 변경을 적용한 복사본을 반환한다

7.1.3 단항 연산자 오버로딩

  • -a 와 같이 한 값에만 작용하는 단항 연산자
// 단항 연산자 정의하기
operator fun Point.unaryMinus() : Point {  //단항연산자는 파라미터가 없습니다.
   return Point(-x, -y)  //각 좌표에 -(음수)를 취한 좌표를 반환
}

>>> val point = Point(10, 20)
>>> println(-point)

Point(x = -10, y = -20)
  • 단항 연산자를 오버로딩하기 위해 사용하는 함수는 인자를 취하지 않는다
함수 이름
+aunaryPlus
-aunaryMinus
!anot
++a, a++inc
--a, a--dec

7.2 비교 연산자 오버로딩

7.2.1 동등성 연산자: equals

  • == 연산자/ != 연산자 호출은 equals 메소드 호출로 컴파일한다

  • == / != 는 내부에서 인자가 널인지 검사하므로 다른 연산과 달리 널이 될 수 있는 값에도 적용할 수 있다

  • 식별자 비교
    === 연산자를 사용해 같은 객체를 가리키는지 비교

  • equals 함수에는 override가 붙어있다
    -> 다른 연산자 오버로딩 관례와 달리 equals는 Any에 정의된 메소드이므로 override가 필요하다
    -> Any의 equals에는 operator가 붙어있기 때문에 하위 클래스 메소드 앞에는 붙이지 않아도 된다

  • Any에서 상속받은 equals가 확장 함수보다 우선순위가 높기 때문에 equals를 확장 함수로 정의할 수 없다

7.2.2 순서 연산자: compareTo

  • 비교 연산자 ( < , > , <= , >= ) 는 compareTo 호출로 컴파일이 됩니다
  • equals와 마찬가지로 Comparable의 compareTo에도 operator 변경자가 붙어있으므로 하위 클래스의 오버라이딩 함수에 operator를 붙일 필요 없다

7.3 컬렉션과 범위에 대해 쓸 수 있는 관례

7.3.1 인덱스로 원소레 접근: get과 set

  • 인덱스 연산자를 사용해
    원소를 읽는 연산은get 연산자 메소드로 변환
    원소를 쓰는 연산은 set 연산자 메소드로 변환

  • get 메소드의 파라미터로 Int가 아닌 타입도 사용할 수 있다
    -> map 같은 경우, matrix[row, col]

7.3.2 in 관례

  • in
    객체가 컬렉션에 들어있는지 검사
    -> in 연산자와 대응하는 함수는 contains이다

7.3.3 rangeTo 관례

  • 범위를 만들기 위해 .. 구문 을 사용해야 한다


ex) 1부터 10 -> 1..10 / 1부터 10전까지 -> 1 until 10

  • 범위 연산자는 다른 산술 연산자보다 우선순위가 낮기 때문에 괄호로 인자를 감싸주면 좋다

7.3.4 for 루프를 위한 iterator 관례

  • for 루푸는 범위 검사와 같이 in 연산자를 사용
    -> 이 경우의 in은 iterator()를 호출해서 이터레이터를 얻은 다음, 자바와 마찬가지로 그 이터레이터에 대해 hasNext와 next 호출을 반복하는 식으로 변환된다

ex) for( i in list )


7.4 구조 분해 선언과 component 함수

  • 구조 분해를 사용하면 복합적인 값을 분해해서 여러 다른 변수를 한꺼번에 초기화할 수 있다
val p = Point(10, 20)
val (x, y) = p // x와 y를 한 번에 초기화한다
// x = 10, y = 20

  • data 클래스의 주 생성자에 들어있는 프로퍼티에 대해서는 컴파일러가 자동으로 componentN 함수를 만들어준다
// 직접 만든다고 한다면
class Point(val x: Int, val y: Int) {
   operator fun component1() = x
   operator fun component2() = y
}
  • 구조 분해 선언은 함수에서 여러 값을 반환할 때 유용
// 값을 저장하기 위한 데이터 클래스를 선언
data class NameComponents(val name: String, val extension: String)

fun splitFileName(fullName: String): NameComponents {
    val result = fullName.split('.', limit = 2)
    
    // 함수에서 데이터 클래스의 인스턴스를 반환
    return NameComponents(result[0], result[1])
}

fun main() {
    // 구조 분해 선언 구문을 사용해 데이터 클래스를 푼다
    val (name, ext) = splitFileName("example.kt")
    println("name : $name, ext : $ext")
}
name : example, ext : kt


-----------------------------------------------
// 컬렉션에 대해 구조 분해 선언 사용하기
data class NameComponents(val name: String, val extension: String)

fun splitFileName(fullName: String): NameComponents {
	val (name, extention) = fullName.split('.', limit = 2)
    return NameComponents(name, extention)
}

7.4.1 구조 분해 선언과 루프

  • 맵의 원소에 대해 이터레이션할 때 구조 분해 선언이 유용하다
// 구조 분해 선언을 사용해 맵 이터레이션 하기
fun printEntries(map: Map<String, String>) {
    // 루프 변수에 구조 분해 선언을 사용
    for((key, value) in map) {
        println("$key -> $value")
    }
}

fun main() {
    val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
    printEntries(map)
}

Oracle -> Java
JetBrains -> Kotlin

-----------------------------------------------
// Map.Entry에 대한 확장 함수로 component1과 component2를 제공
for (entry in map.entries) {
	val key = entry.component1()
	val value = entry.component2()
}

7.5 프로퍼티 접근자 로직 재활용: 위임 프로퍼티

  • 위임 프로퍼티 를 사용하면 값을 뒷받침하는 필드에 단순히 저장하는 것보다 더 복잡한 방식으로 작동하는 프로퍼티를 쉽게 구현할 수 있다
    -> 접근자 로직을 매번 구현할 필요도 없다

  • 프로퍼티 위임을 사용해 자신의 값을 필드가 아니라 데이터베이스 테이블, 브라우저 세션, 맵 등에 저장할 수 있다
    -> 이런 기반에는 위임이 있다

  • 위임
    객체가 직접 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴을 말한다
    -> 작업을 처리하는 도우미 객체를 위임 객체(delegate)라고 부른다

7.5.1 위임 프로퍼티 소개

class Foo {
	var p: Type by Delegate()
}
  • p 프로퍼티는 접근자 로직을 닫른 객체에게 위임한다
    -> Delegate 클래스의 인스턴스를 위임 객체로 사용한다

  • by 뒤에 있는 식을 계산해서 위임에 쓰일 객체를 얻는다
    프로퍼티 위임 객체가 따라야 하는 관례를 따르는 모든 객체를 위임에 사용할 수 있다

// 위임 관례를 따르는 클래스는 getValue, setValue 메소드를 제공해야함
class Delegate {
    // getValue는 게터를 구현하는 로직을 담음
    operator fun getValue(...) { ... }
    // setValue는 세터를 구현하는 로직을 담음
    operator fun setValue(...) { ... }
}

class Foo {
    // by 키워드는 프로퍼티와 위임 객체를 연결
    var p: Type by Delegate()
}

>>> val foo = Foo()
>>> val oldValue = foo.p
>>> foo.p = newValue
  • foo.p는 일반 프로퍼티처럼 쓸 수 있다
    -> 하지만 실제로 게터/세터는 Delegate 타입의 위임 프로퍼티 객체에 있는 메소드를 호출한다

7.5.2 위임 프로퍼티 사용: by lazy()를 사용한 프로퍼티 초기화 지연

  • 지연 초기화
    객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 쓰이는 패턴

  • 초기화 과정에 자원을 많이 사용하거나
    객체를 사용할 때마다 꼭 초기화하지 않아도 되는 프로퍼티에 대해 지연 초기화 패턴을 사용할 수 있다

  • 위임 프로퍼티는 데이터를 저장할 때 쓰이는 뒷받침하는 프로퍼티와 값이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화해준다

// 이메일 클래스
class Email(val content: String)

// 이메일을 가져오는 메소드
fun loadEmails(person: Person): List<Email> {
    println("${person.name}의 이메일 목록")
    return listOf(/*...*/)
}

class Person(val name: String) {
    val emails by lazy { loadEmails(this) }
}
  • lazy함수는 getValue 메서드가 들어있는 객체를 반환한다
    -> lazy를 by 키워드가 함께 사용해 위임 프로퍼티를 만들 수 있다

  • lazy 함수의 인자는 값을 초기화할 때 호출할 람다

  • lazy 함수는 기본적으로 thread-safe하지만, 필요에 따라 동기화에 사용할 lock을 lazy 함수에 전달할 수도 있고 다중 스레드 환경에서 사용하지 않을 프로퍼티를 위해 lazy 함수가 동기화를 하지 못하게 막을 수도 있다

7.5.3 위임 프로퍼티 구현

  • 위임 프로퍼티 예시
    -> 어떤 객체의 프로퍼티가 바뀔 때마다 리스너에게 변경 통지를 보내고 싶을 때

  • 자바에서는 PropertyChangeSupport 와 PropertyChangeEvent 클래스를 사용해 이런 통지를 처리하는 경우가 존재
    ->
    PropertyChangeSupport 클래스는 리스너의 목록을 관리
    PropertyChangeEvent 이벤트가 들어오면 목록의 모든 리스너에게 이벤트를 통지

1) 위임 프로퍼티 없이 프로퍼티 변경 통지를 직접 구현
class Person (val name: String, age: Int, salary: Int): PropertyAware() {
	var age: Int = age
    	set(newValue) {
        	val oldValue = field
            field = newValue
            changeSupport.firePropertyChange(
            "age", oldValue, newValue)
        }
	var salary: Int = salary
    	set(newValue) {
        	... // 위와 동일
        }
}

val p = Person("Kim",29,150)
p.addPropertyChangeListener( ... )  <- 프로퍼티 변경 리스너를 추가한다

이 코드는 field 키워드를 사용해 age 와 salary 프로퍼티를
뒷받침하는 필드에 접근하는 방법을 보여준다

중복값이 많이 보인다

------------------------------------------------------------
2) 도우미 클래스를 통해 프로퍼티 변경 통지 구현하기
class ObservableProperty(
	val propName: String, var propValue: Int,
    val changeSupport: PropertyChangeSupport
) {
	fun getValue() : Int = propValue
    fun setValue(newValue: Int) {
    	val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(propName, oldValue, newValue)
    }
}

class Person(
	val name: String, age: Int, salary: Int): PropertyChangeAware() {
    	val _age = ObservableProperty("age",age, changeSupport)
        var age: Int
        	get() = _age.getValue()
            set(value) = { _age.setValue(value) }
		
        var _salary = ObservableProperty("salary",salary, changeSupport)
        	...
    }

로직의 중복을 상당 부분을 제거했다
하지만 아직도 각각의 프로퍼티마다 ObservableProperty를 만들고 게터/세터에서 ObservableProperty에 작업을 위임하는 부분이 많다

------------------------------------------------------------

3) 위임 프로퍼티를 통해 프로퍼티 변경 통지
class ObservableProperty(
	var propValue: Int, val changeSupport: PropertyChangeSupport
) {
	operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue

	operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
    	val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}

class Person(val name: String, age: Int, salary: Int): PropertyChangeAware() {
	var age: Int by ObservableProperty(age, changeSupport)
    var salary: Int by ObservableProperty(salary, changeSupport)
}

getValue/setValue 에도 operator 변경자가 붙는다
  • by 키워드를 사용해 위임 객체를 지정하면 직접 짜야 했던 코딩들을 컴파일러가 자동으로 처리해준다

  • by 오른쪽에 오는 객체를 위임 객체라고 부른다

  • 위임 객체를 감춰진 프로퍼티에 저장하고, 주 객체이 프로퍼티를 읽거나 쓸 때마다 위임 객체의 getValue/setValue를 호출해준다

// Delegates.observable을 사용해 프로퍼티 변경 통지 구현하기
class ObservableProperty(
	var propValue: Int, val changeSupport: PropertyChangeSupport
) {
	private val observer = {
    	prop: KProperty<*>, oldValue: Int, newValue ->
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
    var age: Int by Delegates.observable(age, observer)
    var salary: Int by Delegates.observable(salary, observer)
  • by의 오른쪽에 있는 식은 새 인스턴스가 아니여도 된다
    -> 우항에 있는 식을 계산한 결과는 컴파일러가 호출할 수 있게 올바른 타입의 get/setValue를 제공해야 한다

7.5.4 위임 프로퍼티 컴파일 규칙

  • 컴파일러는 모든 프로퍼티 접근자 안에 getValue/setValue 호출 코드를 생성해준다

7.5.5 프로퍼티 값을 맵에 저장

  • 확장 가능한 객체
    프로퍼티를 동적으로 정의할 수 있는 객체(확장 가능한 객체)를 만들 때 위임 프로퍼티를 활용하는 경우
// 값을 맵에 저장하는 위임 프로퍼티 사용하기
class Person {
    private val _attributes = hashMapOf<String, String>()
    
    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }
    // 위임 프로퍼티를 맵에 사용
    val name: String by _attributes
}
  • 이런 코드가 작동하는 이유
    표준 라이브러리가 Map과 MutableMap 인터페이스에 대해 getValue와 setValue 확장 함수를 제공하기 때문이다

7.5.6 프레임워크에서 위임 프로퍼티 활용


📌참고자료

Delegation - 1
Delegated Property -2

Kotlin의 Delegates 알아보기 - observable, vetoable

profile
개발자희망자

0개의 댓글