본문 바로가기

Study/Kotlin

Kotlin / 클래스 알아보기

반응형

1. 클래스 알아보기

클래스 정의

지시자는 클래스의 상속이나 클래스 멤버들의 외부에서 접근 범위를 제한한다.

 

- 상속지시자

  • open : 상속할 수 있는 클래스를 만들 때는 반드시 지정해야 한다.
  • final : 코틀린은 상속을 못 하는 클래스가 기본이라서 표시하지 않으면 상속할 수 없는 클래스이다.

- 사용 가시성 지시자 : 클래스를 사용할 수 있는 범위를 지정하는 지시자

  • 비공개 (private) : 파일에 지정하면 그 파일 내부에서만 사용할 수 있다.
  • 상속만 허용 (protected) : 파일 내부나 상속한 경우에만 사용할 수 있다.
  • 모듈만 허용 (internal) : 프로젝트 내의 컴파일 단위의 모듈에서만 사용할 수 있다. 모듈의단위는 maven, Gradle 등에서 지정한 범위에 따른다.
  • 공개 (public) : 어디서나 사용할 수 있다. 공개가 기본이므로 지정하지 않으면 공개 지시자로 처리한다.

다음은 클래스에 필요한 요소들이다.

 

클래스 머리부

  • 클래스 정의 키워드 (class) : 클래스를 정의할 때 필수 예약어
  • 클래스 이름 : class 예약어 다음에 클래스 이름을 작성 (파스칼 케이스 사용)
  • 주 생성자 (primary constructor) : 클래스 이름 옆에 생성자 키워드 constructor를 다음에 정의할 수 있다. 이 예약어 생략 가능
  • 클래스 / 인터페이스 상속 : 기본 생성자 다음에 콜론(:)을 붙이고 클래스는 하나만 올 수 있고, 인터페이스는 여러개 작성할 수 있다.

클래스 몸체부

  • 초기화 블록 
    • 주 생성자가 호출될 때 초기화 블록 내부의 코드가 실행.
    • init 블록보다 위에 선언된 멤버 변수, 생성자 변수만 사용 가능
  • 보조 생성자
    • 본문에 constructor 이름으로 보조 생성자 정의.
    • 함수의 오버로딩처럼 여러 개 정의 가능
    • 주 생성자와 같이 정의된 경우 보조 생성자 중 하나는 반드시 주 생성자를 this로 호출해야한다.
    • 보조 생성자를 정의할 때는 주 생성자처럼 매개변수를 속성으로 지정할 수 없다.
  •  멤버 변수(속성)
    • val 또는 var 키워드로 내부 변수 즉 속성을 정의한다.
    • 주 생성자일 경우만 매개변수로 선언 또는 속성 선언을 선택해서 할 수 있다.
    • 매개변수에 var나 val을 붙이면 속성이다.
  • 매개 함수(메서드)
    • 클래스로 생성되는 객체의 행위를 하는 함수인 메서드를 정의한다.
    • 자바처럼 정적 메서드 즉 클래스로 직접 접근하는 메서드는 없다.
  • 내부 클래스 / 내부 object 정의
    • 클래스 내부에 내포 클래스, 이너 클래스를 정의할 수 있다.
    • object 정의와 동반 객체 선언을 할 수 있다.
public class ClassName constructor(val property1: Int) : Any() {
    init {
        println("초기화 실행")
    }

    var property2: String = "초기화 값"

    constructor(parameter1: String, parameter2: Int) : this(parameter2) {
        var property3: String = parameter1
    }

    fun method1(): Unit {
        // 객체의 행위
    }

    class InnerClass {
        // 클래스 로직
    }

    object InnerObject {
        // 객체 로직
    }
}

위의 클래스는 최상위 클래스 Any를 상속했다. 상속한 클래스가 항상 먼저 로딩이 된다. 

부생성자에는 매개변수가 2개 있고 콜론 다음에 주 생성자 위임호출을 this로 처리하면서 주 생성자의 속성에 해당하는 매개변수를 전달했다. <= 주 생성자와 보조 생성자를 연결해 속성 생성에 대한 순서를 명확히 맞출 수 있다.

생성자(constructor)로 객체 만들기

주 생성자로 객체 생성

class Person(name: String, age: Int) { // 주 생성자의 매개변수 지정
    val name = name // 속성 정의와 매개 변수로 초기화
    var age = age
}

class People(val name: String, val age: Int) // 본문이 속성을 주 생성자에 표시

val c = Person("아프리카", 33) // 객체 인스턴스 생성
val d = c // 객체 인스턴스 연결 : 동일한 객체

println(d.name) // 아프리카
println(c.name) // 아프리카

val p = People("사우디", 33)
println(p.name + " " + p.age) // 사우디 33

Person 클래스의 주 생성자에 매개변수를 정의했고 클래스 코드 내의 속성을 갱신한다.

People 클래스는 주 생성자에 val/var를 붙여 속성으로 만들었다.

 

초기화 블록 실행

class Init(name: String, age: Int) {
    var name: String = ""
    var age: Int = 0

    init { // 초기화 블록 정의
        this.name = name // 쵝화 블록에서는 생성자의 매개변수 사용
        this.age = age // 속성 이름 앞에 this로 현재 객체 표시
        println(" 주 생성자와 같이 실행") // 주 생성자와 같이 실행
    }
}

val i = Init("윤돌",20) 
println(i.name + " " + i.age) //윤돌 20

초기화 블록은 주 생성자가 호출된 다음에 호출됨 -> 그 내부에 속성을 현재 객체 this로 접근해서 속성에 인자로 전달된 값을 할당

 

주 생성자나 보조 생성자가 없는 클래스 정의

class NoConstructor{
    val phoneNo : Int = 123 // 내부 속성
    val name: String = "후순봇"
    var job : String = ""
    var etc : String = ""
    init{ // 초기화 블록은 객체 생성할 때마다 처리됨
        println("초기화 처리")
    }
}

val pno = NoConstructor() // 객체 인스턴스 생성
println(pno.name + " " + pno.phoneNo)
val pno1 = NoConstructor() // 객체 인스턴스 생성
println(pno1.name + " "+pno1.phoneNo)

// 초기화 처리
// 후순봇 123
// 초기화 처리
// 후순봇 123

주 생성자나 보조 생성자 없이 코드 블록에 속성값을 할당한 클래스는 객체를 만들어도 속성값은 항상 같다.

각 객체의 상태를 관리하기 위해서는 생성자를 작성해서 객채를 만들어야 한다.

주 생성자가 없는 것이 아니라 Empty Constructor는 생략할 수 있다.

 

보조 생성자 오버로딩

class PhoneNote(val phoneNo: Int, val name: String) {
    var job: String = ""
    var etc: String = ""

    init {
        println("초기화 처리")
    }

    constructor(phoneNo: Int, name: String, job: String) : this(phoneNo, name) {
        this.job = job
    }
    constructor(phoneNo: Int, name:String, job: String, etc:String):this(phoneNo, name, job){
        this.etc = etc
    }
}

val pno11 = PhoneNote(1234,"이은정","개발자","학생할래") // 매개변수가 4개인 보조 생성자 호출
println(pno11.name + " "+pno11.phoneNo)
val pno22 = PhoneNote(12345,"이은종","피씨방사장님","프로그래머") // 매개변수가 3개인 보조 생성자 호출
println(pno22.name + " "+pno22.phoneNo)

//초기화 처리
//이은정 1234
//초기화 처리
//이은종 12345

보조 생성자도 함수처럼 클래스 내부에서 보조 생성자를 매개변수가 다르게 재정의(오버로딩) 할 수 있다.

이때 생성자에 대한 위임 호출을 정의해야 한다.

멤버 속성과 멤버 메서드 활용

주 생성자 가시성 처리

class C private constructor(val a: Int) { // 비공개 생성자
    companion object {
        private val bar = 100 // 컴패니언 객체 내의 비공개 속성
        fun create(x: Int): C { // 컴패니언 객체 내에서 생성자 메서드로 정의해서 처리
            return C(x)
        }

        fun getBar() = bar // 공개 메서드로 조회
    }

    fun foo() = getBar() // 컴패니언 객체의 비공개 속성의 결과를 조회
}

val cC = C.create(200)
println(cC.foo()) // 100

주 생성자에 비공개 가시성을 지정하면, 객체를 생성할 수 있지만 동반 객체 내에 객체를 생성하는 메서드를 정의할 수 있다.

이렇게 하면 내부 멤버는 비공개 멤버를 참조할 수 있다.

클래스의 멤버인 foo 메서드는 동반 객체 내의 보호 속성 bar를 반환하는 getBar 메서드를 실행해서 결과를 반환한다.

메서드 참조(method reference)

메서드 참조 : 연산자 앞에 클래스나 객체, 연산자 뒤에 메서드 이름을 사용하면 로딩된 메서드의 레퍼런스를 가져온다.

참조 연산자는 더블 콜론(::)이다.

 

2. 상속 알아보기

코틀린 클래스는 기본 final로 지정되어 있어서 기본적으로 상속이 불가능 하다.

상속이 필요한 경우는 항상 클래스를 open 즉 상속할 수 있게 만들어야 한다.

클래스 내부의 속성이나 메서드도 기본이 final이라 서브 클래스에서 재정의 하려면 open 지시자를 붙여줘야 한다.

상속

- 상속관계(is-a) : 상속을 하면 실제 슈퍼클래스와 서브클래스가 하나로 묶여서 사용된다. 클래스 상속관계에 상속하는 클래스를 표시한다.

- 연관관계(has-a) : 클래스를 사용하는 관계. 클래스 내부에 다른 클래스를 속성으로 처리한다.

- 구현관계(implements) : 인터페이스의 추상 메서드나 추상 속성은 실제 구현 클래스에서 모두 구현해야 한다. 

open class Super{
    override fun toString() = "Super(id=${this.hashCode()})"
    open fun info() = "슈퍼 클래스 정보 확인 "
    fun getSuper() = "슈퍼 클래스의 메소드" // 재정의 불가
}
class Sub: Super(){
    override fun toString()  = "Sub(id=${this.hashCode()})"
    override fun info() = "서브 클래스 정보 확인"
}

val sup = Super()
println(sup)
println(sup.info())

val sub = Sub()
println(sub)
println(sub.info())
println(sub.getSuper())

// Super(id=2094548358)
// 슈퍼 클래스 정보 확인 
// Sub(id=455896770)
// 서브 클래스 정보 확인
// 슈퍼 클래스의 메소드

 

상위 클래스 내의 속성와 메서드가 모두 open되면 하위 클래스는 모든 것을 재정의할 수 있다.

그래서 하위 클래스에 필요한 것만 재정의할 수 있도록 지시자로 제한을 둔다.

- open, override 지시자는 하위 클래스에서 재정의할 수 있다.

- 상속을 못 하게 제약하려면 override 앞에 final을 추가해야 한다.

함수를 override할때 final로 더이상 override안되게 할 수 있고 추가로 protected와 같은 가시성도 변경할 수 있다.

open class Person1(val name: String) { // 슈퍼클래스
    fun sayHello() = "안녕하세요" // 재정의 불가
    open fun sayBye() = "안녕히계세요" // 하위클래스에서 재정의 가능
    override fun toString() = "Person(name=$name)" // 상위클래스 메서드 재정의
}

open class Man(name: String, val age: Int) : Person1(name) {
    final override fun sayBye() = "안녕계세요 + $name" // 하위 재정의 금지
    override fun toString() = "Man(name=$name, age=$age)" // 상위 클래스 메서드 재정의
}

class Student(val school: String, name: String, age: Int) : Man(name, age) {
    override fun toString() = "Student(school=$school, name=$name, age=$age" // 상위 클래스 메서드 재정의
}

val pn = Person1("더님") // 슈퍼클래스 1레벨 객체 생성
println(pn)
println(pn.sayBye())

val mn = Man("너님",33) // 슈퍼클래스 2레벨 객체 생성
println(mn)
println(mn.sayBye())

val st = Student("초등학교","달님",11) // 서브클래스 객체 생성
println(st)
println(st.sayBye())

//Person(name=더님)
//안녕히계세요
//Man(name=너님, age=33)
//안녕계세요 + 너님
//Student(school=초등학교, name=달님, age=11
//안녕계세요 + 달님

위의 코드는 3단계 상속을 처리한 것이다.

 

상속에 따른 생성자 호출

상속관계일 때 슈퍼클래스의 멤버를 서브클래스에서 사용한다. 따라서 슈퍼클래스의 속성을 먼저 처리해야 한다.

생성자 호출순서 역시 부모클래스의 생성자가 호출되고 그 다음에 자식 클래스의 생성자가 호출되어야 한다. 

 

open class Animal(val species: String) // 슈퍼클래스 주 생성자

class Pet(species: String, val subSpecies: String) : Animal(species) {
    constructor(species: String, subSpecies: String, age: Int) : this(species, subSpecies) // 슈퍼클래스 위임호출
}

val pet = Pet("개","푸들",4) // 객체 생성
println("종 : ${pet.species} 세부종 : ${pet.subSpecies}") // 속성 출력

//종 : 개 세부종 : 푸들

위 코드는 두 클래스를 작성하고 주 생성자, 보조 생성자, 슈퍼클래스 위임호출을 확인한 것이다.

생성자의 실행순서 아래와 같은 순으로 처리된다.

- 슈퍼클래스의 생성자 -> 서브클래스의 주 생성자 -> 서브클래스의 보조 생성자

3. 다양한 클래스 알아보기

클래스 내부에도 특정 기능을 처리하는 클래스를 정의할 수 있다.

클래스를 정의하는곳

  • 패키지 단위 : 최상위 레벨의 클래스를 정의. 일반적인 클래스 정의
  • 클래스 단위 : 내포 클리스와 이너 클래스를 정의. 
  • 함수 단위 : 지역 클래스는 함수안에 정의. 함수 내부는 외부에서 접근할 수 없는 지역 영역이라 클래스도 지역 영역에서만 사용 가능.

내포 클래스 (Nested class)

내포 클래스는 특정 클래스를 내부에 정의한 것이다.

- 아무런 지시자도 추가하지 않고 정의한다. 클래스 내부에 정의되어 있지만 별도 클래스이다.

- 내포 클래스를 외부에서 사용할 때는 외부 클래스 이름으로 접근해서 사용한다. 

- 이 내포 클래스는 자신을 정의한 외부 클래스 내의 속성에 직접 접근할 수 없다.

- 내포 클래스의 객체를 생성할 때는 외부 클래스 이름으로 접근해서 내포 클래스 생성자를 호출해 객체를 만든다. 그런 다음 내부에 있는 메서드를 호출해서 처리한다.

class Outer {
    private val bar: Int = 1 // 외부 클래스의 비공개 속성

    class Nested {
        private val nestVar = 100 // 내포된 클래스
        fun foo() = 999 // 내포된 클래스의 메서드에서 외부 클래스 멤버

        // fun foo() = this@Outer.bar // 외부 클래스 멤버 참조시 예외 발생
    }
}

val demo = Outer.Nested() // 내포된 객체 생성은 외부클래스로 접근해서 생성
println(demo.foo()) // 내포된 객체의 메서드 실행
// Outer.Nested().nestVar // 내포 클래스의 비공개 속성 접근 시 예외 발생

//999

이너 클래스 (Inner class)

클래스 내부에 클래스를 정의할 때 inner 예약어를 붙여서 이너 클래스를 정의할 수 있다.

내포 클래스와 달리 이너 클래스는 외부 클래스의 속성을 참조할 수 있다. 또한, 이너 클래스는 멤버처럼 객체로 접근해서 객체를 생성한다.

이너 클래스의 객체는 this, 외ㄴ부 클래스의 객체는 this@외부 클래스 이름으로 사용한다.

class Outer2 {
    private val bar: Int = 1 //외부 클래스의 비공개 속성

    inner class Inner {
        private val bar = 100 // 동일한 이름의 속성을 가지고 있음
        fun foo() = this@Outer2.bar // 내포된 클래스의 메서드 외부 비공개 속성 접근
        fun fbar() = bar // 비공개 속성에 접근할 수 있는 메서드 제공
    }

    fun getBar() = println(Inner().fbar())
}

val demo2 = Outer2().Inner().foo() // 이너 클래스가 멤버 클래스라서 객체로 접근
println(demo2)
Outer2().getBar()

//1
//100

inner class는 잘 사용하지 않는다.

inner class에서 outter class의 property나 method에 바로 접근 할 수 있는 형태는 추후 class를 분리 하는 시점에 비용이 많이 들어서 하지 않고, 외부에서 생성자를 호출하려면 결국 outter class를 생성하고 생성해야 해서 비추 한다.

지역 클래스 (Local class)

함수 기능이 복잡해지면 여러 개의 지역함수만으론 처리가 곤란해질 수 있는데, 이때 지역클래스를 정의해서 사용할 수 있다.

fun localClasses() {
    open class Amphibian { // 함수 내부에 지역 베이스 클래스 정의
        open fun foo() = "foo"
    }

    class Frog : Amphibian() { // 상속받아서 지역 클래스 정의
        override fun foo() = "bar"
    }

    val amphibian: Amphibian = Frog() // 객체 생성
    println(amphibian.foo()) // 메서드 호출
}

localClasses()

//bar

메서드에서 전역변수 참조

메서드는 객체의 속성을 주로 처리하지만, 함수처럼 전역변수로 참조해서 사용할 수 있다.

var ar : Int = 999 // 전역 할당

class AB{
    fun methodsA(a:Int):Int{ // 메서드
        ar += a // 전역 갱신
        return ar // 전역 참조
    }
}
println(AB().methodsA(10))// 전역 갱신 결과
println(ar)

//1009
//1009

메서드도 객체의 속성이 없으면 함수처럼 전역변수를 참조해서 처리한다.

클래스의 메서드에 전역변수를 갱신하고 반환을 할 수 있다.

실제 전역변수도 변경 여부를 확인하면 메서드 내부에서 변경된 값과 동일하다.

 

메서드 내부에서 메서드 외부에 있는 값을 변경하는 건 좋지 않다. 퓨어함수가 아니라서 사이드 이펙트가 발생할 케이스가 생긴다.

 

외부 클래스의 상속관계를 이너 클래스에서 처리

상속관계를 가지는 외부 클래스를 정의하면 이너 클래스에서 속성을 참조할 때 상속관계되 지정해야 한다.

  • this : 이너 클래스의 객체는 항상 this로 참조
  • this@외부 클래스 : 이너 클래스에서 외부 클래스의 속성은 외부 클래스를 @ 다음에 표기해야 참조가능
  • super@외부 클래스 : 외부 클래스의 상속관계는 this 대신 super를 사용
open class Base{
    open fun method() = println("베이스 클래스 f()")
}
class Derived:Base(){
    override fun method() = println("파생 클래스 f()")
    inner class Inner{
        fun method() = println("이너클래스 f()")
        fun test(){
            this.method() // 이너 클래스 메서드 참조
            Derived().method() // 외부 클래스 메서드 참조
            super@Derived.method() // 슈퍼 클래스 메서드 참조
        }
    }
}

val c1 = Derived()
c1.Inner().test()

//이너클래스 f()
//파생 클래스 f()
//베이스 클래스 f()

4. object 알아보기

object 예약어는 클래스 정의와 하나의 객체 생성을 동시에 한다.

그래서 object 예약어를 사용하면 하나의 객체만 만들어진다. 이런 행위를 하는 패턴을 싱글턴 객체 패턴이라고 한다.

object를 처음 사용할 때 초기화된다는 것은 중요하다.

object 표현식(expression)

익명의 클래스로 익명의 객체를 만들 필요가 있을 때 object 표현식으로 하나의 객체를 만든다.

/* 지역 변수에 익명 클래스 생성 - 일회성 객체가 필요한 경우 내부에 정의해서 바로 사용 */
fun getLength(): Double {
    val point = object {
        val x: Double = 2.0
        val y: Double = 3.0
        override fun toString() = "Point($x, $y)"
    }
    println(point)
    return sqrt(point.x.pow(2.0) + point.y.pow(2.0))
}
println(getLength())
// Point(2.0, 3.0)
// 3.605551275463989

/* 함수의 매개변수에 익명 객체 전달 */
interface Personnel { // 자료형으로 사용할 인터페이스 정의
    val name: String
    val age: Int
}
fun getObject(p: Personnel): Personnel { // 함수 매개변수와 반환값을 인터페이스로 자료형 지정
    return p
}

val p = getObject(object : Personnel { // 인자로 object 표현식을 생성된 객체 전달
    override val name = "달문" // 인터페이스 내의 추상 속성을 구현
    override val age = 55
})

println("객체 반환 이름 = ${p.name} 나이 = ${p.age}")
// 객체 반환 이름 = 달문 나이 = 55

object 정의

object 정의는 하나의 싱글턴 패턴을 만드는 방법이다.

 

object 정의를 처음으로 사용할 때 메모리에 로딩된다.

object 정의의 이름으로 코드 블록에 정의돈 속성이나 메서드를 사용할 수 있다.

내부에 객체와 클래스를 정의할 수 있다.

object Counter{
    private var count: Int = 0 // 비공개 속성 정의
    fun currentCount() = count // 비공개 속성 조회
    fun increment() = ++count // 비공개 속성 갱신
}
Counter.increment()
println(Counter.currentCount()) // 1

동반 객체 (companion object)처리

클래스 내부에 object 정의는 실제 클래스와 상관없이 작동하는 객체를 만드는 것이다.

클래스와 상호작용을 하는 동반 객체도 정의할 수 있다.

자바의 static 느낌

class ObjectClass {
    object ObjectTest { // 싱글턴 객체 생성
        const val CONST_STRING = "1"
        fun test() {
            println("object 선언 : $CONST_STRING")
        }
    }
}

class CompanionClass {
    companion object { // 동반객체 정의
        const val CONST_TEST = 2
        fun test() {
            println("동반 객체 선언 : $CONST_TEST")
        }
    }
}

CompanionClass.test()
ObjectClass.ObjectTest.test()
//동반 객체 선언 : 2
//object 선언 : 1

object정의는 클래스 이름.객체 이름.메서드를 호출하지만 동반객체는 클래스 이름.메서드 이름으로 더 간단히 메서드를 호출한다.

클래스와 동반객체는 하나처럼 움직이도록 구성되어 있기 때문.

클래스에 동반 객체를 정의하면 다양한 기능을 클래스 이름으로 처리할 수 있다.

5. 확장 알아보기

클래스의 기능을 추가하려면 기존 클래스를 수정해야 하는데,

코틀린 언어는 클래스를 직접 수정하지 않고 클래스에 기능을 추가하는 방법을 제공한다. 이를 확장(extension)이라고 한다.

일반 속성(property)과 확장 속성(extension property)

코틀린의 속성은 기본 배킹필드인 field를 제공하고 getter/setter 메서드를 제공한다.

변경할 수 없는 속성을 val로 정의할 때는 get 메서드가 만들어진다.

변경할 수 있는 속성을 var로 정의할 경우는 get과 set 메서드가 만들어진다. 

확장 속성일 때 이 두가지는 직접 정의해서 처리한다.

 

최상위 속성

최상위 속성은 field를 사용할 수 있다 -> 게터와 세터를 정의할 때 실제 값을 보관하는 필드를 사용해서 처리한다.

val person: Int = 0
    get():Int { // getter 메서드
        return field // 속성의 배킹 필드
    }

var man: Int = 0
    get() = field // getter 메서드
    set(value) { // setter 메서드
        field = value // 속성의 배킹 필드에 갱신
    }
    
println(person) // 0
man = 100
println(man) // 100

속성 확장

확장 속성을 정의하려면 어떤 클래스에 확장할 지를 리시버 클래스를 지정해야 한다.

object나 동반 객체에도 확장 가능하다.

확장함수

확장 속성처럼 메서드도 클래스나 object에 추가할 수 있다. 

확장함수가 호출되면 이 리시버의 객체 즉 현재 객체를 받아서 객체가 접근이 가능한 속성이나 메서드를 사용할 수 있다.

비공개 속성이나 메서드는 접근할 수 없다.

fun Int.swap(other: Int): Pair<Int, Int> {
    var (first, second) = other to this
    return first to second
}

println((100).swap(200)) // (200, 100)

내장클래스 Int에 두 개의 값을 바꾸는 확장함수를 정의하였다.

 

멤버와 확장의 주의할 사항

확장 속성이나 확장 함수는 클래스의 멤버가 아니다.

클래스의 멤버와 이름이 충돌날 경우에 멤버가 항상 우선이다. (멤버가 비공개일 경우만 확장이 우선)

클래스와 확장함수 사용

클래스를 정의해서 사용하는 것은 편리하지만 클래스의 기능을 변경하여 매번 컴파일하고 배포하는 것은 어렵다.

확장함수는 필요한 경우에 작성해서 사용하므로 편리하다. 

 

클래스 구현 방식

  • 여러 클래스에 처리하는 메서드를 인터페이스로 지정
  • 클래스는 단일 책임성 원칙에 따라 분리
  • 기능의 확장은 추가 기능에는 열려 있고 변경에는 닫혀있는 오픈-클로즈의 원칙에 따라 확장함수를 사용
  • 기능을 처리하는 경우 클래스 위임으로 권한을 위임
interface Balanceable {
    fun getBal(): Double // 잔액 조회 추상 메서드
    fun credit(amount: Int) // 입금 추상 메서드
}

class Balance(var balance: Double) : Balanceable {
    override fun getBal() = balance
    override fun credit(amount: Int) {
        balance += amount.toDouble()
    }
}

class AgreementManager(val balance: Balanceable) : Balanceable by balance {
}

fun AgreementManager.calBenefit(rate: Double) { // 이자 계산 후 입금 처리 확장함수로 구현
    val benefit = balance.getBal() * rate / 365 // 연 이자로 계산
    balance.credit(benefit.toInt())
}

val agreeMG = AgreementManager(Balance(100.00))
agreeMG.credit(10000)
println(agreeMG.getBal()) // 10100.0
agreeMG.calBenefit(0.5)
println(agreeMG.getBal()) // 10113.0