반응형
Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- 루씬 인 액션
- K번째수
- 완주하지 못한 선수
- 커링
- 쿠버네티스
- 검색 기능 확장
- 가장 큰 수
- 정렬
- 전화번호 목록
- @Getter
- @EnableScheduling
- 고차원 함수
- 알고리즘
- @configuration
- Java
- 다리를 지나는 트럭
- 프로그래머스
- 기능개발
- 해시
- 스프링 스케쥴러
- 모던 자바 인 액션
- 영속 자료구조
- 코딩 테스트
- @Data
- 롬복 어노테이션
- @Setter
- kubenetes
- 스택/큐
- H-index
- 크론 표현식
Archives
- Today
- Total
Today I Learned
델리게이션을 통한 확장 (1) 본문
728x90
상속 대신 델리게이션을 써야 하는 상황
- 클래스의 객체가 다른 클래스의 객체가 들어갈 자리에 쓰여야 한다면 상속을 사용해라
- 클래스의 객체가 단순히 다른 클래스의 객체를 사용만 해야 한다면 델리게이션을 사용해라
델리게이션을 사용한 디자인
디자인적 문제점
interface Worker {
fun work()
fun takeVacation()
}
class JavaProgrammer : Worker {
override fun work() = println("...write java...")
override fun takeVation() = println("...code at the beach...")
}
class CSharpProgrammer : Worker {
override fun work() = println("...write C#...")
override fun takeVation() = println("...branch at the ranch...")
}
class Manager
- 위 코드는 작업자인 Worker 인터페이스와 Worker를 구현한 두 개의 프로그래머 클래스, 팀을 관리하기 위한 매니저 클래스이다.
- Worker에게 일을 시키기위해 Manger를 사용하도록 디자인하는 것은 쉽지 않다.
잘못된 경로로의 상속
- Manager를 JavaProgrammer에서 상속받으면 Manager 클래스에서 구현을 다시 작성할 필요가 없다. 상속으로 디자인해보자.
open class JavaProgrammer : Worker {
Class Manager : JavaProgrammer()
val doe = Manager()
doe.work() //...write Java...
- 하지만 이렇게하면 Manager 클래스가 JavaProgrammer 클래스에 갇혀버리게 된다.
어려운 델리게이션
- 먼저 Java에서 Manager가 Worker에게 델리게이션을 사용하는 방식을 코틀린 코드로 나타내보자.
class Manager(val worker: Worker) {
fun work() = worker.work()
fun takeVataion() = worker.work()
}
val doe = Manager(JavaProgrammer())
doe.work() //...write Java...
- Manager 인스턴스를 만든 후 JavaProgrammer 인스턴스를 생성자로 전달했다.
- 이렇게하면 Manager의 인스턴스를 JavaProgrammer, CSharpProgrammer 등 Worker 인터페이스를 구현한 클래스의 인스턴스에게 위힘할 수 있다.
- 또한 더이상 JavaProgrammer에 open을 입력할 필요가 없다.
- 하지만 이런 디자인은 코드가 장황할 뿐만 아니라, 소프트웨어 디자인의 기본사항 몇가지도 어기고 있다.
- Manager는 Worker의 인스턴스를 호출하는 기능만 가지고 있고, 모든 호출 코드는 호출할 메소드명을 제외하고는 거의 비슷하다. 이는 DRY(Don't Repeat yourself) 원칙을 위반한다.
- 그리고 Work 인터페이스에 deploy() 메소드를 추가한다면 Manger가 해당 메소드를 위임하는 호출을 하기 위해 Manager 클래스도 변경해야 한다. 이는 OCP(Open-Closed Pinciple) 위반이다.
코틀린의 by 키워드를 사용한 델리게이션
class Manager() : Worker by JavaProgrammer()
- 코드 작성 시점에 Manager는 어떤 메소드도 가지고있지 않다.
- 코틀린 컴파일러는 Worker에 속하는 Manager 클래스의 메소드를 바이트코드 수준에서 구현하고, by 키워드 뒤에 나오는 JavaProgrammer 클래스의 인스턴스로 호출을 요청한다.
- 즉, by 키워드가 컴파일 시간에 이전 예제에서 수동으로 구현했던 델리게이션을 대신 해준다.
val doe = Manger()
doe.work() //...write Java...
- 상속이 아니기 때문에 Manger의 인스턴스를 JavaProgrammer 타입의 참조가 필요한 곳에서 사용할 수 없다.
val coder = JavaProgrammer = doe // ERROR: type mismatch
파라미터에 위임하기
- Worker by JavaProgrammer() 코드에선 명시적으로 생성된 JavaProgrammer의 인스턴스로 델리게이트한다.
- 이렇게 되면 Manager 클래스의 인스턴스는 오직 JavaProgrammer의 인스턴스에만 요청해야 한다.
- 또한 Manger 클래스 안에 다른 메소드를 작성하더라도 해당 메소드에서는 델리게이션에 접근할 수 없다.
- 이런 제약은 인스턴스를 생성하면서 델리게이션을 지정하지 않고 생성자에 델리게이션 파라미터를 전달함으로써 해결 가능하다.
class Manager(val staff: Worker) : Worker by staff {
fun meeting() = println("meeting with ${staff.javaClass.simpleName}")
}
- staff가 Manger 객체의 속성이기 때문에 meeting() 메소드로 staff에 접근할 수 있다.
- work() 메소드를 호출할땐 staff가 델리게이션이기 때문에 staff로 요청이 전달된다.
val doe = Manager (CSharpProgrammer())
val roe = Manager (JavaProgrammer())
doe.work() //...write C#...
doe.meeting() //meeting with CShrapProgrammer
roe.work()//...write Java...
roe.meeting() //meeting with JavaProgrammer
메소드 충돌 관리
- 코틀린에서는 델리게이션을 이용하는 클래스가 델리게이션 클래스의 인터페이스를 구현해야한다.
- 하지만 실제로는 인터페이스의 각 메소드를 모두 구현하지 않는다.
- 델리게이션 클래스의 모든 인터페이스를 위해 코틀린 컴파일러가 만든 랩퍼를 만든다.
- 하지만 델리게이션 클래스가 인터페이스의 메소드를 구현하지 않은 경우엔 델리게이션을 이용하는 클래스에서 메소드를 구현해야한다.
- 델리게이션 클래스가 인터페이스의 메소드를 이미 구현한 상태에서 델리게이션을 이용하는 클래스에서 메소드를 다시 구현하려고 하는 경우엔 override 키워드를 사용해야 한다.
class Manager(val staff: Worker) : Worker by staff {
override fun takeVacation() = println("of course")
}
- 클래스는 여러 개의 인터페이스 역시 델리게이션할 수 있다.
- 만약 인터페이스 사이에서 메소드 충돌이 있다면 후보 클래스가 충돌된 메소드를 오버라이드해야 된다.
interface Worker {
fun work()
fun takeVacation()
fun fileTimeSheet() = println("Why? really?")
}
interface Assistant {
fun doChores()
fun fileTimeSheet() = println("NO escape from that")
}
class Manager(val staff:Worker, val assistant : Assistant) :
Worker by staff, Assistant by assistant {
override fun fileTimeSheet() {
print("manually forwarding this..")
assistant.fileTimeSheet()
}
}
- Manger 인스턴스가 fileTimeSheet() 메소드의 호출을 가로채서 Worker와 Assistant에 있는 메소드가 충돌하거나 임의의 메소드로 실행되는 것을 방지할 수 있었다.
- Manager 인스턴스는 수동으로 Worker에게 요청할지 혹은 Assistant에게 요청할지 아니면 둘다에게 요청할지 결정할 수 있다.
델리게이션 주의사항
- 지금까지 Manager는 JavaProgrammer의 인스턴스에게 델리게이션을 요청했다. 하지만 Manager의 참조는 JavaProgrammer의 참조에 할당될 수 없다. 델리게이션은 상속과 다르게 우연히 대체될 가능성이 없는 재사용성을 제공해준다.
- 하지만 델리게이션을 사용할 클래스는 위임할 인터페이스를 구현해야한다. 따라서 델리게이션을 사용하는 클래스를 참조하면 위임할 인터페이스의 참조에 할당될 수 있다.
//불가
val coder: JavaProgrammer = doe //ERROR : type mismatch
//가능
val employee: Worker = doe
- 델리게이션의 진짜 목적은 Manager가 Worker를 이용하는 것이지만, 코틀린의 델리게이션 구현의 부작용으로 Manger는 Worker로 취급된다.
- 또한 델리게이션 속성 선언을 val이 아닌 var로 하게 되면 몇 가지 문제가 발생한다.
class Manager(var staff: Worker) : Worker by staff
val doe = Manager(JavaProgrammer())
prinln("Staff is #{doe.staff.javaClass.simpleName}") //Staff is JavaProgrammer
doe.work() //..write Java..
doe.staff=CSharpProgrammer()
prinln("Staff is #{doe.staff.javaClass.simpleName}") //Staff is CSharpProgrammer
doe.work() //..write Java..
- staff 객체에는 두 개의 참조가 있다. 하나는 클래스 안에 백킹 필드로서 존재하는 참조, 또 하나는 델리게이션의 목적으로 존재하는 참조이다.
- staff 속성을 CSharpProgrammer 클래스로 변경하면 필드만 변경되고 델리게이션의 참조를 변경한 것은 아니기 때문에 위와 같은 결과가 발생한다.
728x90
'Kotlin > 다재다능 코틀린 프로그래밍' 카테고리의 다른 글
함수형 코틀린 (1) (0) | 2023.09.08 |
---|---|
델리게이션을 통한 확장 (2) (0) | 2023.09.08 |
클래스 계층과 상속 (0) | 2023.08.24 |
객체와 클래스 (0) | 2023.08.01 |
오류를 예방하는 타입 안정성 (2) (0) | 2023.08.01 |
Comments