코틀린은 open 키워드를 통해 상속 매개변수를 지원하지만, 언어의 주요 권장 사항은 상속 대신 컴포지션에 동의하는 것입니다. 이는 취약한 계층 및 깊은 상속과 관련된 문제를 피하면서 보다 유연하고 확장 가능한 시스템을 구축할 수 있게 합니다.
컴포지션은 필요한 유형의 객체를 클래스의 필드로 포함시키고 그의 작업을 위임하는 것을 포함합니다. 코틀린은 by 키워드를 사용하여 인터페이스의 구현을 객체에 자동으로 위임할 수 있게 하여 위임 패턴을 쉽게 만듭니다.
위임 패턴 예:
interface Logger { fun log(message: String) } class ConsoleLogger : Logger { override fun log(message: String) = println(message) } class Service(private val logger: Logger) : Logger by logger { fun doAction() { log("Action done") } } fun main() { val service = Service(ConsoleLogger()) service.doAction() // 출력: Action done }
이러한 접근 방식은 코드 재사용을 용이하게 하고 로직을 보다 모듈화합니다.
"data class가 다른 클래스, 예를 들어 추상 클래스에서 상속받을 수 있나요?"
data class는 다른 클래스(인터페이스 제외)로부터 상속받을 수 없습니다. data class는 항상 final이기 때문입니다. 예외는 인터페이스로, 이는 구현할 수 있습니다.예:
abstract class Base(val name: String) data class Derived(val age: Int, val name: String) : Base(name) // 컴파일 오류: data class는 Base 클래스를 확장할 수 없음
하지만 가능합니다:
interface User data class Admin(val name: String, val rights: List<String>) : User
이야기
프로젝트에서 여러 서비스를 공통의 추상 클래스에서 상속받아 반복되는 로직을 구현하기로 했습니다. 결과적으로 많은 상속 레벨이 생기고 디버깅이 복잡해지며 테스트 문제도 발생하였습니다. 컴포지션과 위임(인터페이스 및 의존성 주입을 통해)으로 전환한 후 코드를 단순화하고 더 모듈화하여 테스트 커버리지를 증가시킬 수 있었습니다.
이야기
초보 개발자는 다른 클래스를 사용하여 데이터 클래스를 확장하여 공통 기능을 추가하려고 했습니다. 코드가 컴파일되지 않았지만 프로그래머는 오랫동안 이유를 이해하지 못했습니다(코틀린의 data class 제한).
이야기
복잡한 로깅 로직이 있는 프로젝트에서 로깅 기능을 기본 클래스에 통합하기로 했습니다. 그러나 시스템이 성장함에 따라 일부 서비스는 다른 로깅 구현을 요구하게 되었습니다. Logger 인터페이스 및 위임을 통한 컴포지션을 사용하여 리팩토링할 수 밖에 없었으며, 이는 아키텍처를 크게 단순화했습니다.