본문 바로가기
Kotlin/Kotlin 프로그래밍

[Kotlin] 클래스와 객체 (2)

by 주 녕 2021. 4. 19.
728x90

모든 내용은 Do it! 코틀린 프로그래밍을 바탕으로 정리한 것입니다. 

 

super과 this의 참조

상위 클래스는 super, 현재 클래스는 this로 참조

super로 상위 객체 참조

open class Bird(var name: String, var wing: Int, var beak: String, var color: String) {
    fun fly() = println("Fly wing: $wing")
    open fun sing(vol: Int) = println("Sing vol: $vol")
}

class Parrot : Bird {
    val language: String
    constructor(name: String, wing: Int, beak: String, color: String, language: String) : super(name, wing, beak, color) {
        this.language = language
    }
    fun speak() = println("Speak! $language")
    override fun sing(vol: Int) {
        super.sing(vol)
        println("I'm a parrot!")
        speak()
    }
}
  • super 키워드로 상위 클래스의 프로퍼티, 메서드, 생성자를 사용할 수 있음
    • super.프로퍼티, super.메서드(), super()
    • 상위 클래스에서 구현한 내용을 그대로 사용하고 필요한 내용만 추가하고 싶은 경우

this로 현재 객체 참조

여러 개의 부 생성자 참조하기

open class Person {
    constructor(firstName: String) {
        println("[Person] firstName: $firstName")
    }
    constructor(firstName: String, age: Int) {  // 3번
        println("[Person] firstName: $firstName, $age")
    }
}

class Developer: Person {
    constructor(firstName: String): this(firstName, 10) {  // 1번
        println("[Developer] $firstName")
    }
    constructor(firstName: String, age: Int): super(firstName, age) {  // 2번
        println("[Developer] $firstName, $age")
    }
}

fun main() {
    val sean = Developer("Sean")
}
  1. 인자가 1개 이므로 Developer의 부 생성자 1번으로 진입
  2. Developer의 부 생성자 1번에서 this()가 있으므로 부 생성자 2번 호출
  3. Developer의 부 생성자 2번에서 super()가 있으므로 부모 클래스의 생성자 3번 호출
  4. Person의 부 생성자 3번 실행
  5. Developer의 부 생성자 2번 실행
  6. Developer의 부 생성자 1번 실행

주 생성자와 부 생성자 함께 사용하기

class Person(firstName: String, out: Unit = println("[Primary Constructor] Parameter")){
    val fName = println("[Property] Person fName: $firstName")
    init {
        println("[init] Person init block")
    }

    constructor(firstName: String, age: Int,
            out: Unit = println("[Secondary Constructor] Parameter")): this(firstName) {
        println("[Secondary Constructor] Body: $firstName, $age")
    }
}

fun main() {
    val p1 = Person("Kildong", 30)
    println()
    val p2 = Person("Dooly")
}

p1 객체 생성

  1. 인자가 2개 이므로 Person의 부 생성자로 진입
  2. Person의 부 생성자에 this()가 있으므로 주 생성자 호출
  3. fName 프로퍼티 할당
  4. init 초기화 블록 실행
  5. Person의 부 생성자 실행

p2 객체 생성

  1. 인자가 1개 이므로 Person의 주 생성자로 진입
  2. fName 프로퍼티 할당
  3. init 초기화 블록 실행

바깥 클래스 호출하기

클래스를 선언할 때 클래스 안에 다시 클래스를 선언하는 것이 가능함

특정 클래스의 내부에 선언된 클래스 : 이너 클래스 / 이너 클래스의 바깥에 있는 클래스 : 바깥 클래스

open class Base {
    open val x: Int = 1
    open fun f() = println("Base Class f()")
}

class Child : Base() {
    override val x: Int = super.x +1
    override fun f() = println("Child Class f()")

    inner class Inside {
        fun f() = println("Inside Class f()")
        fun test() {
            f()
            Child().f()
            super@Child.f()
            println("[Insdie] super@Child.x: ${super@Child.x}")
        }
    }
}

fun main() {
    val c1 = Child()
    c1.Inside().test()
}
  • 바깥 클래스의 메서드/프로퍼티를 사용하는 경우 : 바깥클래스().메서드()/프로퍼티
  • 바깥 클래스의 부모 클래스의 메서드/프로퍼티를 사용하는 경우 : super@바깥클래스.메서드()/프로퍼티

인터페이스 참조하기

인터페이스 : 일종의 구현 약속. 인터페이스를 참조하는 클래스는 인터페이스가 가지고 있는 내용을 구현해야 하는 가이드를 제시

* 코틀린에서는 자바와 마찬가지로 클래스의 다중 상속이 지원되지 않음

* 인터페이스는 필요한 만큼 다수의 인터페이스를 지정해 구현할 수 있음 → 인터페이스의 프로퍼티/메서드의 이름 중복 가능

open class A {
    open fun f() = println("A Class f()")
    fun a() = println("A Class a()")
}

interface B {
    fun f() = println("B Interface f()")
    fun b() = println("B Interface b()")
}

class C : A(), B {
    override fun f() = println("C Class f()")
    fun test() {
        f()
        b()
        super<A>.f()
        super<B>.f()
    }
}

fun main() {
    val c = C()
    c.test()
}
  • 이름이 중복되지 않은 메서드는 바로 사용 가능
  • 이름이 중복된 메서드의 경우 : super<클래스명/인터페이스명>.메서드()

 


정보 은닉 캡슐화

캡슐화 (Encapsulation) : 클래스를 작성할 때 속성이나 기능을 숨기는 것 → 정보 은닉

가시성 지시자

가시성 (Visibility) : 클래스, 메서드, 프로퍼티의 접근 범위

민감하거나 불필요한 부분은 숨기고 사용하기 위해 필요한 부분만 공개 → 클래스/메서드/프로퍼티에 가시성 지시자로 지정 가능

  • private : 이 요소는 외부에서 접근할 수 없음
  • public : 이 요소는 어디서든 접근이 가능함 (디폴트)
  • protected : 외부에서 접근할 수 없으나 하위 상속 요소에서는 가능함
  • internal : 같은 정의의 모듈 내부에서는 접근 가능함

private

: 접근 범위가 선언된 요소에 한정하는 가시성 지시자

private class PrivateClass {
    private var i = 1
    private fun privateFunc() {
        i += 1
    }
    fun access() {
        privateFunc()
    }
}

class OtherClass {
    // val opc = PrivateClass() - 프로퍼티 opc는 private가 되어야 함
    fun test() {
        val pc = PrivateClass()
    }
}

fun main() {
    val pc = PrivateClass()
    // pc.i
    // pc.privateFunc()
}

fun TopFunction() {
    val tpc = PrivateClass()
}
  • 같은 파일에서 private로 지정한 객체를 생성할 수 있음
    • 다른 클래스에서 프로퍼티로서 객체를 지정하려면 프로퍼티도 똑같이 private로 선언해야 함
    • 객체를 생성했다고 하더라도 private 클래스의 멤버는 다른 클래스나 main(), 최상위 함수에서 접근 불가
  •  private 멤버는 해당 클래스 내부에서만 접근이 가능

protected

: 최상위에 선언된 요소에는 지정할 수 없고 클래스나 인터페이스와 같은 요소의 멤버에만 지정할 수 있는 가시성 지시자

open class Base {
    protected var i = 1
    protected fun protectedFunc() {
        i += 1
    }
    fun access() {
        protectedFunc()
    }
    protected class Nested
}

class Derived : Base() {
    fun test(base: Base): Int {
        protectedFunc()
        return i
    }
}

fun main() {
    val base = Base()
    // base.i
    // base.protectedFunc()
    base.access()
}
  • 멤버가 클래스인 경우에도 protected를 사용할 수 있음
  • protected로 지정된 멤버는 상속된 하위 클래스에서 자유롭게 접근이 가능함
  • 외부 클래스나 객체 생성 후 점(.) 표기를 통해 protected 멤버에 접근하는 것은 허용하지 않음

internal

* internal : 프로젝트 단위의 모듈

* 기존의 자바에서는 package라는 지시자에 의해 패키지 이름이 같은 경우에 접근을 허용함* 코틀린에서는 패키지에 제한하지 않고 하나의 모듈 단위를 대변하는 internal를 사용

 

가시성 지시자와 클래스의 관계

open class Base {
    // 이 클래스에서는 a, b, c, d, e 접근 가능
    private val a = 1
    protected open val b = 2
    internal val c = 3
    val d = 4
    
    protected class Nested {
        // 이 클래스에서는 b, c, d, e 접근 가능
        public val e: Int = 5
        private val f: Int = 6
    }
}

class Derived : Base() {
    // 이 클래스에서는 b, c, d, e 접근 가능
    // a는 접근 불가
    override val b = 5
}

class Other(base: Base) {
    // base.a, base.b는 접근 불가
    // base.c, base.d는 접근 가능(같은 모듈)
    // Base.Nested에는 접근 불가, Nested::e 접근 불가
}
  • Base 클래스에서 파생된 Derived 클래스 : protected, internal, public으로 지정된 프로퍼티에 접근 가능
  • 외부 클래스인 Other 클래스 : internal, public으로 지정된 프로퍼티만 접근 가능
  • 오버라이딩 된 멤버가 있는 경우 상위 클래스와 동일한 가시성 지시자를 갖음

 


클래스와 클래스의 관계

  • 약한 참조 관계 : 연관(Association), 의존(Dependency) → 어떤 객체에서 또 다른 객체를 '이용한다' / 생명주기가 다름
  • 집합(Aggregation) 관계 : 연못과 오리의 관계 → 연못과 오리는 따로 떨어져도 문제가 없음
  • 합성/구성(Composition) 관계 : 자동차와 엔진의 관계 → 자동차가 파괴되면 엔진도 동작하지 않음 / 생명주기가 의존

연관 관계

: 2개의 서로 분리된 클래스가 연결을 가지는 것

→ 단방향/양방향으로 연결될 수 있으며, 두 요소가 서로 다른 생명주기를 가지고 있음

class Patient(val name: String) {
    fun doctorList(d: Doctor) {
        println("Patient: $name, Doctor: ${d.name}")
    }
}

class Doctor(val name: String) {
    fun patientLst(p: Patient) {
        println("Doctor: $name, Patient: ${p.name}")
    }
}

fun main() {
    val doc1 = Doctor("KimSabu")
    val patient1 = Patient("Kildong")
    doc1.patientLst(patient1)
    patient1.doctorList(doc1)
}
  • Doctor와 Patient 클래스의 객체는 따로 생성되며 서로 독립적인 생명주기를 가지고 있음
  • 위의 예시에서는 두 클래스가 서로의 객체를 참조하는 양방향 참조를 가짐

의존 관계

: 한 클래스가 다른 클래스에 의존되어 있어 영향을 주는 경우

class Patient(val name: String, var id: Int) {
    fun doctorList(d: Doctor) {
        println("Patient: $name, Doctor: ${d.name}")
    }
}

class Doctor(val name: String, val p: Patient) {
    val customerId: Int = p.id
    fun patientLst(p: Patient) {
        println("Doctor: $name, Patient: ${p.name}")
        println("Patient Id: $customerId")
    }
}

fun main() {
    val patient1 = Patient("Kildong", 1234)
    val doc1 = Doctor("KimSabu", patient1)
    doc1.patientLst(patient1)
    patient1.doctorList(doc1)
}
  • Doctor 클래스에서 Patient를 매개 변수로 받아야 하므로 Patient 객체가 먼저 생성되어 있어야 함
  • Doctor 클래스는 Patient 클래스에 의존함

집합 관계

: 연관 관계와 거의 동일하지만 특정 객체를 소유한다는 개념이 추가된 것

class Pond(_name: String, _members: MutableList<Duck>) {
    val name: String = _name
    val members: MutableList<Duck> = _members
    constructor(_name: String): this(_name, mutableListOf<Duck>())
}

class Duck(val name: String)
    fun main() {
        val pond = Pond("myFavorite")
        val duck1 = Duck("Duck1")
        val duck2 = Duck("Duck2")

        pond.members.add(duck1)
        pond.members.add(duck2)

        for (duck in pond.members) {
            println(duck.name)
        }
    }
  • 연못은 개념적으로 여러 오리를 소유할 수 있고, 오리는 한번에 한 연못에서만 놀 수 있음
    • 오리를 여럿 추가하기 위해 배열/리스트 구조가 필요함
    • 2개의 개체는 따로 생성되어 서로의 생명주기에 영향을 주지 않음

구성 관계

: 집합 관계와 거의 동일하지만 특정 클래스가 어느 한 클래스의 부분이 되는 것

→ 소유자 클래스가 삭제되면 구성하고 있던 클래스도 같이 삭제됨 (생명주기가 의존)

class Car(var name: String, val power: String) {
    private var engine = Engine(power)

    fun startEngine() = engine.start()
    fun stopEngine() = engine.stop()
}

class Engine(power: String) {
    fun start() = println("Engine has been started")
    fun stop() = println("Engine has been stopped")
}

fun main() {
    val car = Car("tico", "100hp")
    car.startEngine()
    car.stopEngine()
}
  • Engine 클래스는 Car 클래스의 생명주기에 의존적
    • car 객체를 생성함과 동시에 Engine 클래스의 객체도 생성됨
728x90

댓글