본문 바로가기
Study/다재다능 코틀린 프로그래밍

Day 9. 델리게이션을 통한 확장

반응형

본 포스트는 지인들과 스터디한 내용을 정리한 포스트입니다

 

http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&ejkGb=KOR&barcode=9788931463422

 

다재다능 코틀린 프로그래밍 - 교보문고

다양한 프로그래밍 패러다임으로 살펴보는 코틀린 기본서 | 코틀린은 멀티패러다임 프로그래밍 언어다. 코틀린은 스크립트로 사용할 수도 있고, 객체지향 코드나 함수형 코드, 비동기 프로그램

www.kyobobook.co.kr


1. 델리게이션을 통한 확장

  • 상속과 델리게이션 모두 객체지향 프로그래밍의 디자인 방식이다
  • 두 방식 모두 클래스를 다른 클래스로부터 확장시킨다
  • 상속과 델리게이션 차이
    • 상속은 베이스 클래스(부모)에 강력하게 묶이고 수정할 수 없게 된다
    • 델리게이션은 객체 자신이 처리해야 할 일을 다른 클래스의 인스턴스에게 위임하거나 넘겨버릴 수 있다
  • Java는 상속에 대해 많은 지원을 해줬지만 델리게이션에 대해서는 지원이 약하다

 

2. 상속 대신 델리게이션을 써야 하는 상황

  • 클래스의 객체가 다른 클래스의 객체가 들어갈 자리에 쓰여야 한다면 상속을 사용해라
  • 클래스의 객체가 단순히 다른 클래스의 객체를 사용만 해야 한다면 델리게이션을 사용해라
  • 상속의 단점
    • 정적 타입 객체지향 언어에서의 상속을 통한 다형성의 매력이라고 생각하고 사용한다
    • 베이스 클래스에서 상속받은 인스턴스를 자식 클래스에서 마음대로 바꾸려는 행동은 오류를 일으킬 수 있다
    • 리스코프 치환원칙(LSP)
    • 자식 클래스에서 부모 클래스의 메소드를 오버라이드할 때 베이스 클래스의 외부 동작을 유지해아하는 문제가 있다 ( 제약사항 )
  • 클래스의 구현을 새로 하거나 한 클래스의 인스턴스를 '개는 동물이다'와 같이 포함 관계에 있는 다른 클래스로 대체할 때 상속을 사용하라
  • 오직 다른 객체의 구현을 재사용하는 경우라면 델리게이션을 사용하라

 

3. 델리게이션을 사용한 디자인

  • 기존 Java의 델리게이션 디자인
    // project.kts
    interface Worker {
        fun work()
        fun takeVacation()
    }
    
    
    class JavaProgrammer : Worker {
        override fun work() {
            println("...write Java...")
        }
    
        override fun takeVacation() {
            println("...code at the beach...")
        }
    }
    
    
    class CSharpProgrammer : Worker {
        override fun work() {
            println("...write C#...")
        }
    
        override fun takeVacation() {
            println("...branch at the ranch...")
        }
    }
    
    
    class Manager(private val worker: Worker) {
        fun work() = worker.work()
        fun takeVacation() = worker.takeVacation()
    }
    
    
    val javaProgrammer = JavaProgrammer()
    val manager = Manager(javaProgrammer)
    manager.work() // ...write Java...
    manager.takeVacation() // ...code at the beach...​
    • Manager는 단순히 호출의 기능만 수행한다
    • Worker에 더 많은 기능이 들어가면 또 Manager에 더 많은 호출 코드가 들어간다
    • DRY(Don't Repeat Yourself) 원칙을 위반하는 나쁜 디자인이다
    • 또한 OCP(개방 폐쇄 원칙)도 위반했는데, 변경에는 닫혀있어야하고, 확장에는 열려있어야하는 원칙에서 봤을 때 Worker가 변경되면 Manager 또한 코드변경이 불가피하므로 좋지 않은 디자인이다
  • 코틀린의 by 키워드를 사용한 델리게이션
    // projectWithDeligation.kts
    interface Worker {
        fun work()
        fun takeVacation()
    }
    
    
    class JavaProgrammer : Worker {
        override fun work() {
            println("...write Java...")
        }
    
        override fun takeVacation() {
            println("...code at the beach...")
        }
    }
    
    
    class CSharpProgrammer : Worker {
        override fun work() {
            println("...write C#...")
        }
    
        override fun takeVacation() {
            println("...branch at the ranch...")
        }
    }
    
     class Manager() : Worker by JavaProgrammer()
    
    val doe = Manager()
    doe.work() // ...write Java...
    doe.takeVacation() // ...code at the beach...​
    • Worker by JavaProgrammer() 를 보면 by라는 키워드를 통해 간단히 인터페이스로 요청을 위임하는 델리게이션을 구현했다
    • by 키워드가 컴파일 시간에 이전 예제에서 우리가 시간을 들여서 수동으로 구현했던 델리게이션을 대신 해준다
    • Manager 클래스는 JavaProgrammer 클래스를 상속받은 것이 아니다
    • 코틀린의 델리게이션을 사용하면, 컴파일러가 내부적으로 Manager 클래스에 메소드를 생성하고 요청을 한다

 

4. 파라미터에 위임하기

  • 위에서 보았던 by 키워드를 이용해 객체를 생성하면서 델리게이션하는 방식은 문제점이 있다
    • Manager 클래스의 인스턴스는 오직 JavaProgrammer의 인스턴스에만 요청할 수 있다 ( 다른 종류의 Worker에는 요청 불가능 )
    • Manager의 인스턴스는 델리게이션에 접근할 수 없다 ( Manager 클래스 안에 다른 메소드를 작성하더라도 해당 메소드에서는 델리게이션에 접근할 수 없다 )
  • 인스턴스를 생성하면서 델리게이션을 지정하지 않고 생성자에 델리게이션 파라미터를 전달함으로써 해결이 가능하다
    // projectWithDeligationAnother.kts
    interface Worker {
        fun work()
        fun takeVacation()
    }
    
    
    class JavaProgrammer : Worker {
        override fun work() {
            println("...write Java...")
        }
    
        override fun takeVacation() {
            println("...code at the beach...")
        }
    }
    
    
    class CSharpProgrammer : Worker {
        override fun work() {
            println("...write C#...")
        }
    
        override fun takeVacation() {
            println("...branch at the ranch...")
        }
    }
    
    class Manager(val staff : Worker) : Worker by staff {
        fun meeting() = println("organizing meeting with ${staff.javaClass.simpleName}")
    }
    
    
    val doe = Manager(CSharpProgrammer())
    doe.work() // ...write C#...
    doe.takeVacation() // ...branch at the ranch...
    doe.meeting() // organizing meeting with CSharpProgrammer
    
    val roe = Manager(JavaProgrammer())
    roe.work() // ...write Java...
    roe.takeVacation() // ...code at the beach...
    roe.meeting() // organizing meeting with JavaProgrammer
    • Manager 클래스는 staff 파라미터를 델리게이션으로 사용한다
    • 위와 같은 델리게이션은 하나의 클래스에 국한되지 않는다 ( 유연성 )
    • Worker 클래스의 메소드를 호출하면 코틀린이 자동으로 연결된 델리게이션으로 요청을 전달한다

 

5. 메소드 충돌 관리

  • 코틀린 컴파일러는 델리게이션에 사용되는 클래스마다 델리게이션 메소드를 위한 랩퍼를 만든다
  • 코틀린에서는 델리게이션을 이용하는 클래스가 델리게이션 클래스의 인터페이스를 구현해야한다
  • 하지만 실제로는 인터페이스의 각 메소드를 모두 구현하지 않는다
  • 델리게이션 클래스가 인터페이스의 메소드를 이미 구현한 상태인 경우 델리게이션을 이용하는 클래스에서 다시 메소드를 구현하려고할땐 override 키워드를 사용해아한다
    // projectWithDeligationOverride.kts
    interface Worker {
        fun work()
        fun takeVacation()
    }
    
    
    class JavaProgrammer : Worker {
        override fun work() {
            println("...write Java...")
        }
    
        override fun takeVacation() {
            println("...code at the beach...")
        }
    }
    
    
    class CSharpProgrammer : Worker {
        override fun work() {
            println("...write C#...")
        }
    
        override fun takeVacation() {
            println("...branch at the ranch...")
        }
    }
    
    class Manager(val staff : Worker) : Worker by staff {
        override fun takeVacation() = println("of course")
    }
    
    
    val doe = Manager(CSharpProgrammer())
    doe.work() // ...write C#...
    doe.takeVacation() // of course
    
    val roe = Manager(JavaProgrammer())
    roe.work() // ...write Java...
    roe.takeVacation() // of course​
  • 코틀린은 메소드 충돌을 훌륭하게 해결해 인터페이스의 특정 메소드를 델리게이션으로 요청하지 않도록 오버라이드 해준다
  • 두개의 인터페이스를 구현하는 경우
    // projectWithDeligationDuplicate
    interface Worker {
        fun work()
        fun takeVacation()
        fun fileTimeSheet() = println("Why? Really?")
    }
    
    class JavaProgrammer : Worker {
        override fun work() {
            println("...write Java...")
        }
    
        override fun takeVacation() {
            println("...code at the beach...")
        }
    }
    
    
    class CSharpProgrammer : Worker {
        override fun work() {
            println("...write C#...")
        }
    
        override fun takeVacation() {
            println("...branch at the ranch...")
        }
    }
    
    interface Assistant {
        fun doChores()
        fun fileTimeSheet() = println("No escape from that")
    }
    
    class DepartmentAssistant : Assistant {
        override fun doChores() = println("routine stuff")
    }
    
    
    class Manager(private val staff: Worker, private val assistant: Assistant) :
        Worker by staff, Assistant by assistant {
    
        override fun takeVacation() = println("of course")
        override fun fileTimeSheet() {
            print("manually forwarding this...")
            assistant.fileTimeSheet()
        }
    }
    
    val doe = Manager(CSharpProgrammer(), DepartmentAssistant())
    doe.work() // ...write C#...
    doe.takeVacation() // of course
    doe.doChores() // routine stuff
    doe.fileTimeSheet() // manually forwarding this...No escape from that​

 

6. 델리게이션 주의사항

  • 지금까지의 예제를 예로 Manager 클래스는 JavaProgrammer를 사용할 수 있지만 Manager를 JavaProgrammer로 사용할 수는 없다
  • 예제코드
    // projectVersion8.kts
    interface Worker {
        fun work()
        fun takeVacation()
        fun fileTimeSheet()  = println("Why? Really?")
    }
    
    class JavaProgrammer : Worker {
        override fun work() = println("...write java...")
        override fun takeVacation() = println("...code at the beach...")
    }
    
    class CSharpProgrammer : Worker {
        override fun work() = println("...write C#...")
        override fun takeVacation() = println("...branch at the ranch...")
    }
    
    class Manager(var staff: Worker) : Worker by staff
    val doe = Manager(JavaProgrammer())
    println("Staff is ${doe.staff.javaClass.simpleName}") // Staff is JavaProgrammer
    doe.work() // ...write java...
    println("changing staff") // changing staff
    doe.staff = CSharpProgrammer() // Staff is CSharpProgrammer
    println("Staff is ${doe.staff.javaClass.simpleName}") // ...write java...
    doe.work()​

    • 위에서 선언된 델리게이션은 속성이 아니라 파라미터이다
    • doe.staff = CSharpProgrammer()로 처음에 선언된 Manager 클래스의 staff 프로퍼티 참조를 바꿔주어도 필드만 변경한 것이지 델리게이션의 참조를 변경한 것이 아니다
    • 위 같은 코드는 또다른 문제를 발생시키는데, 기존의 JavaProgrammer의 인스턴스에 더 이상 접근할 수가 없어졋다(필드를 CSharpProgrammer로 변경했기 때문에), 하지만 델리게이션이 JavaProgrammer를 사용중이기 때문에 가비지컬렉터가 수집해가지도 않는다...
  • 코틀린은 객체의 속성이 아닌 주 생성자에 보내진 파라미터로 델리게이션을 한다

 

7. 변수와 속성 델리게이션

  • 속성이나 지역변수를 읽을 때, 코틀린 내부에서는 getValue() 함수를 호출한다
  • 속성이나 변수를 설정할 때 코틀린은 setValue() 함수를 호출한다
  • 변수 델리게이션
    • 지역변수의 읽기와 쓰기에 대한 접근을 모두 가로챌 수 있다
    • 리턴 되는 것을 변경할 수 있고 데이터를 언제, 어디에 저장하는지도 변경할 수 있다
    • 예제코드(클래스)
      // PoliteString.kt
      package chapter9
      
      import kotlin.reflect.KProperty
      
      class PoliteString(var content: String) {
          operator fun getValue(thisRef: Any?, property: KProperty<*>) = content.replace("stupid", "s*****")
          operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
              content = value
          }
      }​
    • 예제코드(스크립트)
      // politecomment.kts
      import chapter9.PoliteString
      
      var comment: String by PoliteString("Some nice message")
      println(comment) // Some nice message
      comment = "This is stupid"
      println(comment) // This is s*****
      println("comment is of length: ${comment.length}") // comment is of length: 14​
    • 코틀린에선 인터페이스를 구현할 필요도 없고 관습적인 코드도 없다, 단지 메소드만 있으면 된다
    • 메소드 시그니처에 대한 확신이 없다면 kotlin.properties.ReadOnlyProperty와 kotlin.properties.ReadWriteProperty 인터페이스를 참고하면 된다
  • 속성 델리게이션
    • 델리게이션은 getValue()를 구현하는(읽기전용), getValue()와 setValue()를 모두 사용하는(읽기-쓰기) 속성이라면 어떤 객체에서든 사용 가능하다
    • 코틀린 스탠다드 라이브러리의 디자인을 보면 Map과 MutableMap은 델리게이션을 사용할 수 있다
    • Map은 val 속성, MutableMap은 var 속성으로 사용할 수 있다 ( Map은 get(), getValue() 보유, MutableMap은 set(), setValue()까지 보유 )
    • 예제코드
      // postcomment.kts
      import kotlin.reflect.KProperty
      
      class PoliteString(val dataSource: MutableMap<String, Any>) {
          operator fun getValue(thisRef: Any?, property: KProperty<*>): String =
              (dataSource[property.name] as? String)?.replace("stupid", "s*****") ?: ""
      
          operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
              dataSource[property.name] = value
          }
      }
      
      
      class PostComment(dataSource: MutableMap<String, Any>) {
          val title: String by dataSource
          var likes: Int by dataSource
          val comment: String by PoliteString(dataSource)
          override fun toString(): String = "Title: $title Likes: $likes Comment: $comment"
      }
      
      
      val data = listOf(
          mutableMapOf(
              "title" to "Using Delegation",
              "likes" to 2,
              "comment" to "Keep it simple, stupid"),
          mutableMapOf(
              "title" to "Using Inheritance",
              "likes" to 1,
              "comment" to "Prefer Delegation where possible"
          )
      )
      
      val forPost1 = PostComment(data[0])
      val forPost2 = PostComment(data[1])
      forPost1.likes++
      println(forPost1) // Title: Using Delegation Likes: 3 Comment: Keep it simple, s*****
      println(forPost2) // Title: Using Inheritance Likes: 1 Comment: Prefer Delegation where possible​
  • 빌트인 스탠다드 델리게이션
    • 코틀린은 우리가 실제로 유용하게 사용할 만한 몇 가지 빌트인 델리게이션을 제공해준다
      • 지연 델리게이션: 객체 생성이나 연산 실행이 실제 필요할 때까지 실행을 지연시켜주는 유용한 기능
      • observable 델리게이션: 속성의 값이 변하는 것을 지켜보게 해주는 유용한 기능
      • Vetoable 델리게이션: 기본 규칙이나 비즈니스 로직에 기반한 속성이 변경되는 것을 막아줌
    • 지연 델리게이션
      • 개발자는 직접 컴파일러에게 식의 결과가 정말로 필요하기 전까지는 식을 실행하지 않도록 지연 연산을 요청할 수 있다
      • 식의 결과가 필요하지 않으면 식 전체를 스킵해 버린다
      • lazy 래퍼 함수를 이용한 예제코드
        // lazyevaluation.kts
        fun getTemperature(city: String): Double {
            println("fetch from webservice for $city")
            return 30.0
        }
        
        val showTemperature = false
        val city = "Boulder"
        val temperature by lazy { getTemperature(city) }
        if (showTemperature && temperature > 20) // (nothing here)
            println("Warm")
        else
            println("Nothing to report")​
      • lazy 함수는 연산을 실행할 수 있는 람다 표현식을 아규먼트로 받는다, 그 후 필요한 순간에만 실행한다(요청 즉시 실행 X )
      • lazy 함수는 기본적으로 람다 표현식의 실행과 동기화된다, 따라서 하나의 스레드만 실행된다
    • 옵저버블 델리게이션
      • 연관된 변수나 속성의 변화를 가로채는 ReadWriteProperty 델리게이션을 만든다
      • 변화가 발생하면 델리게이션이 개발자가 observable() 함수에 등록한 이벤트 핸들러를 호출한다
      • 예제코드
        // observe.kts
        import kotlin.properties.Delegates
        
        var count by Delegates.observable(0) { property, oldValue, newValue ->
            println("Property: $property old: $oldValue: new: $newValue")
        }
        println("The value of count is: $count")
        count++
        println("The value of count is: $count")
        count--
        println("The value of count is: $count")​
      • 지역변수나 객체의 속성의 변화를 지켜보려고 한다면 옵저버블 델리게이션을 사용하라
      • 모니터링과 디버깅 목적으로 사용할 때 매우 유용하다
      • 지켜보기만 하는 게 아니라 변경을 허가할지, 불허할지 결정하려면 vetoable 델리게이션을 사용하도록 하자
728x90
반응형