본문 바로가기
Program Language/Kotlin

[Do it! 코틀린 프로그래밍] 5. 클래스와 객체

by SungJe 2021. 4. 10.

5. 클래스와 객체


1. 클래스와 객체의 정의

객체지향 프로그래밍(OOP: Object-Oriented Programming) 은 프로그램의 구조를 객체 간 상호작용으로서 표현하는 프로그래밍 방식이다. 객체 지향 기법으로 프로그램을 설계할 때 알아 두어야할 개념은 다음과 같다.

  • 추상화(Abstraction): 특정 클래스를 만들 때 기본 형식을 규정하는 방법
  • 인스턴스(Instance): 클래스로부터 생성한 객체
  • 상속(Inheritance): 부모 클래스의 내용을 자식 클래스가 그대로 물려받음
  • 다형성(Polymorphism): 하나의 이름으로 다양한 처리를 제공
  • 캡슐화(Encapsulation): 내용을 숨기고 필요한 부분만 사용
  • 메시지 전송(Message Sending): 객체 간에 주고받는 메시지
  • 연관(Association): 클래스 간의 관계

객체 지향 프로그래밍의 용어

코틀린에서 사용하는 용어 다른 언어에서 사용하는 용어
클래스(Class) 분류, 범주
프로퍼티(Property) 속성(Attriibute), 변수(Variable), 필드(Field), 데이터(Data)
메서드(Method) 함수(Function), 동작(Operation), 행동(Behavior)
객체(Object) 인스턴스(Instance)

클래스 다이어그램(Class Diagram): 클래스의 정의와 관계를 나타내는 다이어그램

Class Diagram
/* 구조 */
class 클래스 이름 {
    // 프로퍼티
    // 매서드
}

/* Bird 클래스 정의 */
class Bird {
    // 프로퍼티(속성)는 반드시 초기화되어야 한다.
    var name: String = "mybird"
    var wing: Int = 2
    var beak: String = "short"
    var color: String = "blue"

    // 메서드(함수)는 함수를 선언하는 방법과 동일하다.
    fun fly() = println("Fly wing: $wing")
    fun sing(vol: Int) = println("Sing vol: $vol")
}

fun main() {
    val coco = Bird() // 객체 생성
    coco.color = "red"

    println("coco.color: ${coco.color}")
    coco.fly()
    coco.sing(3)
}

NOTE✏️

  • 통합 모델링 언어(UML: Unified Modeling Language): 객체 지향 프로그래밍 소프트웨어 집약 시스템을 개발할 때 산출물을 명세화, 시각화, 문서화할 때 사용한다. Link

2. 생성자

생성자(Constructor) 란 클래스를 통해 객체가 만들어질 때 기본적으로 호출되는 함수를 말한다.

  • 부 생성자(Secondary Constructor): 클래스의 본문에 함수처럼 선언한다.
class Bird {
    // Property
    var name: String
    var wing: Int
    var beak: String
    var color: String

    // Secondary Constructor
    constructor(name: String, wing: Int, _beak: String, _color: String) {
        this.name = name // this 키워드는 객체 자신에 대한 참조
        this.wing = wing
        beak = _beak // this 키워드 대신 언더스코어(_) 이용 가능
        color = _color
    }

    // 두 번째 부 생성자
    constructor(_name: String, _beak: String) {
        name = _name
        wing = 2
        beak = _beak
        color = "grey"
    }

    // Method
    fun fly() = println("Fly wing: $wing")
    fun sing(vol: Int) = println("Sing vol: $vol")
}

fun main() {
    val coco = Bird("coco", 2, "short", "red") // 첫 번째 부 생성자 호출
    val dove = Bird("dove", "short") // 두 번째 부 생성자 호출

    println("coco.color: ${coco.color}")
    coco.fly()
    coco.sing(3)
}
  • 주 생성자(Primary Constructor): 클래스 이름과 함께 생성자를 정의한다. 초기화에 꼭 사용할 코드가 있다면 초기화 블록을 선언해야 한다.
// Primary Constructor: constructor 키워드 생략 가능, 프로퍼티 포함 가능, 기본값 지정 가능
class Bird constructor(var name: String = "NONAME", var wing: Int = 2, var beak: String, var color: String) {
    // Property 주 생성자에 포함시킴

    // Initialization Block
    init {
        println("---------- 초기화 블록 ----------")
        println("이름: $name, 부리: $beak")
        this.sing(3)
        println("----------------------------------")
    }

    // Method
    fun fly() = println("Fly wing: $wing")
    fun sing(vol: Int) = println("Sing vol: $vol")
}

fun main() {
    val coco = Bird(beak = "long", color = "red")

    println("coco.color: ${coco.color}")
    coco.fly()
    coco.sing(3)
}

3. 상속과 다형성

  • 상속(Inheritance): 상위 클래스(부모 클래스)의 속성과 기능을 물려받아 계승하는 것이다. 코틀린에서 open 키워드 없이 선언된 클래스는 최종 클래스로 상속이 불가능하다. Link
Inheritance
/* 구조 */
open class 기반 클래스 이름 { // 묵시적으로 Any 클래스로부터 상속됨
    ...
}
class 파생 클래스 이름 : 기반 클래스 이름() { // 기반 클래스로부터 상속됨
    ...
}

/* Bird 클래스 상속 */
open class Bird(var name: String, var wing: Int, var beak: String, var color: String) {
    // Method
    fun fly() = println("Fly wing: $wing")
    fun sing(vol: Int) = println("Sing vol: $vol")
}

// 주 생성자를 사용한 상속
class Lark(name: String, wing: Int, beak: String, color: String) : Bird(name, wing, beak, color) {
    // Method
    fun singHitone() = println("Happy Song!")
}

// 부 생성자를 사용한 상속
class Parrot : Bird {
    // Property
    val language: String

    // Secondary Constructor
    constructor(name: String,
                wing: Int,
                beak: String,
                color: String,
                language: String) : super(name, wing, beak, color) {
        this.language = language
    }

    // Method
    fun speak() = println("Speak! $language")
}

fun main() {
    val lark = Lark("lark", 2, "long", "brown")
    val parrot = Parrot("parrot", 2, "short", "multiple", "korean")

    lark.singHitone()
    lark.fly()
    parrot.speak()
    parrot.sing(4)
}
  • 다형성(Polymorphism): 메서드가 같은 이름을 사용하지만 구현 내용이 다르거나 매개변수가 달라서 하나의 이름으로 다양한 기능을 수행할 수 있는 개념이다. Link

    • 오버로딩(Overloading): 동일한 클래스 안에서 같은 이름의 메서드가 매개변수만 달리하여 여러 번 정의 될 수 있는 개념
    • 오버라이딩(Overriding): 상위 클래스의 메서드의 이름, 매개변수, 반환값은 동일하나 내용을 새롭게 재정의한다.
/* add 메서드 오버로딩 */
class Calculator {
    fun add(x: Int, y: Int): Int = x + y
    fun add(x: Double, y: Double): Double = x + y
    fun add(x: String, y: String): String = x + y
}

fun main() {
    val calculator = Calculator()

    println(calculator.add(3, 2)) // 5
    println(calculator.add(3.14, 0.4)) // 3.54
    println(calculator.add("Hello ", "Kotlin!")) // Hello Kotlin!
}
/* 메서드 오버라이딩 */
open class Bird(var name: String, var wing: Int, var beak: String, var color: String) {
    // Method
    fun fly() = println("Fly wing: $wing")
    open fun sing(vol: Int) = println("Sing vol: $vol") // open 키워드를 사용하여 오버라이딩 허용
}

class Parrot(name: String,
             wing: Int = 2,
             beak: String,
             color: String,
             var language: String = "natural") : Bird(name, wing, beak, color) {

    // Method
    fun speak() = println("Speak! $language")
    override fun sing(vol: Int) { // override 키워드를 사용하여 메서드 오버라이딩
        println("I'm a parrot! The volume level is $vol")
        speak()
    }
}

open class Lark(name: String, wing: Int, beak: String, color: String) : Bird(name, wing, beak, color) {
    // Method
    fun singHitone() = println("Happy Song!")
    final override fun sing(vol: Int) { // final override 키워드로 하위 클래스에서 재정의를 막음
        println("Sing vol: $vol")
        singHitone()
    }
}

4. super와 this의 참조

클래스의 설계에서 상위와 현재 클래스의 특정 메서드나 프로퍼티, 생성자를 참조해야 하는 경우가 생긴다. 상위 클래스는 super 키워드로, 현재 클래스는 this 키워드로 참조가 가능하다.

참조 대상 상위 클래스 현재 클래스
프로퍼티 super.프로퍼티 이름 this.프로퍼티 이름
메서드 super.메서드 이름() this.메서드 이름()
생성자 super() this()
  • 상위 클래스의 메서드에 필요한 내용만 추가하기
open class Bird(var name: String, var wing: Int, var beak: String, var color: String) {
    // Method
    fun fly() = println("Fly wing: $wing")
    open fun sing(vol: Int) = println("Sing vol: $vol")
}

class Parrot(name: String, wing: Int = 2, beak: String, color: String,
             var language: String = "natural") : Bird(name, wing, beak, color) {
    // Method
    fun speak() = println("Speak! $language")
    override fun sing(vol: Int) {
        super.sing(vol) // 상위 클래스의 sing()을 먼저 수행
        println("I'm a parrot! The volume level is $vol")
        speak()
    }
}
  • this와 super를 사용하는 부 생성자
open class Person {
    constructor(firstName: String) {
        println("[Person] firstName: $firstName")
    }

    constructor(firstName: String, age: Int) { // ③
        println("[Person] firstName: $firstName, age: $age") // ④
    }
}

class Developer: Person {
    constructor(firstName: String): this(firstName, 10) { // ①
        println("[Developer] firstName: $firstName") // ⑥
    }

    constructor(firstName: String, age: Int): super(firstName, age) { // ②
        println("[Developer] firstName: $firstName, age: $age") // ⑤
    }
}

fun main() {
    val sean = Developer("Sean")
}
  • 주 생성자와 부 생성자 함께 사용하기
// ② 주 생성자
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 person1 = Person("Kim", 30) // 실행 순서: ① -> ② -> ③ -> ④ -> ⑤
    println()
    val person2 = Person("Lee") // 실행 순서: ② -> ③ -> ④
}
  • 인터페이스에서 참조하기: 중복된 이름의 프로퍼티나 메서드가 존재할 때, 앵글 브래킷(< >)을 사용하여을 사용하여 충돌을 방지한다.
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()")
    override fun b() = println("C Class b()")

    fun test() {
        b() // 현재 클래스의 b()
        super<B>.b() // 인터페이스 B의 b()

        f() // 현재 클래스의 f()
        super<A>.f() // 클래스 A의 f()
        super<B>.f() // 인터페이스 B의 f()
    }
}

fun main() {
    val c = C()
    c.test()
}

NOTE✏️

  • 인터페이스(Interface): 클래스들이 구현해야 하는 동작을 지정하는데 사용되는 추상 자료형이다. Link

5. 정보 은닉 캡슐화

캡슐화(Encapsulation) 란 객체의 속성(Property)과 행위(Method)를 하나로 묶고, 실제 구현 내용 일부를 외부에 감추어 은닉하는 것이다. Link

가시성 지시자(Visibility Modifier)

Visibility Modifier
  • private: 외부에서 접근할 수 없다.
  • protected: 외부에서 접근할 수 없으나 하위 상속 요소에서는 가능하다.
  • internal: 같은 정의의 모듈 내부에서는 접근이 가능하다.
  • public: 어디서든 접근이 가능하다.(기본값)
open class Base {
    // 이 클래스에서는 a, b, c, d, e 접근 가능
    private val a = 1
    protected open val b = 2
    internal val c = 3
    val d = 4 // 가시성 지시자의 기본값은 public

    protected class Nested {
        // 이 클래스에서는 a, b, c, d, e, f 접근 가능
        public val e: Int = 5 // public 생략 가능
        private val f: Int = 6
    }
}

class Derived : Base() {
    // 이 클래스에서는 b, c, d, e 접근 가능
    // a는 접근 불가
    override val b = 0 // Base의 b는 오버라이딩 됨 - 상위와 같은 protected 지시자
}

class Other(base: Base) {
    // base.a, base.b는 접근 불가
    // base.c, base.d는 접근 가능(같은 모듈안에 있으므로)
    // Base.Nested는 접근 불가, Nested::e 역시 접근 불가
}
/* 자동차와 도둑 예제 */
open class Car protected constructor(_year: Int, _model: String, _power: String, _wheel: String) {
    // Property
    private var year: Int = _year
    public var model: String = _model
    protected open var power: String = _power
    internal var wheel: String = _wheel

    // Method
    protected fun start(key: Boolean) {
        if (key) println("Start the Engine!")
    }

    // Nested Class
    class Driver(_name: String, _license: String) {
        private var name: String = _name
        var license: String = _license // public
        internal fun driving() = println("[Driver] Driving - $name")
    }
}

class Tico(_year: Int, _model: String, _power: String, _wheel: String,
          var name: String, private var key: Boolean) : Car(_year, _model, _power, _wheel) {
    // Property
    override var power: String = "50hp"
    val driver = Driver(name, "first class")

    // Secondary Constructor
    constructor(_name: String, _key: Boolean) : this(2014, "basic", "100hp", "normal", _name, _key) {
        name = _name
        key = _key
    }

    // Method
    fun access(password: String) {
        if (password == "gotico") {
            println("---- [Tico] access() ----")
            // super.year // private 접근 불가
            println("super.model = ${super.model}") // public 접근
            println("super.power = ${super.power}") // protected 접근
            println("super.wheel = ${super.wheel}") // internal 접근
            super.start(key) // protected

            // driver.name // private 접근 불가
            println("Driver().license = ${driver.license}")
            driver.driving() // internal
        } else {
            println("You're a burglar")
        }
    }
}

class Burglar() {
    // Method
    fun steal(anycar: Any) {
        if (anycar is Tico) {
            println("---- [Burglar] steal() ----")
            // println(anycar.power) // protected 접근 불가
            // println(anycar.year) // private 접근 불가
            println("anycar.name = ${anycar.name}") // public 접근
            println("anycar.wheel = ${anycar.wheel}") // internal 접근(같은 모듈 안에 있으므로)
            println("anycar.model = ${anycar.model}") // public 접근

            println(anycar.driver.license) // public 접근
            anycar.driver.driving() // internal 접근(같은 모듈 안에 있으므로)
            // println(Car.start()) // protected 접근 불가
            anycar.access("dontknowo")
        } else {
            println("Nothing to steal")
        }
    }
}

fun main() {
    // val car = Car() // protected 생성 불가
    val tico = Tico("Sungje", true)
    tico.access("gotico")

    val burglar = Burglar()
    burglar.steal(tico)
}

6. 클래스와 클래스의 관계

  • 연관(Association): 2개의 서로 분리된 클래스가 연결을 가지는 관계다. 단방향 혹은 양방향으로 연결되며 두 요소가 서로 다른 생명주기를 가지고 있다.
/* 연관 관계 나타내기 */
class Patient(val name: String) {
    fun doctorList(doctor: Doctor) { // 인자로 참조
        println("Patient: $name, Doctor: ${doctor.name}")
    }
}

class Doctor(val name: String) {
    fun patienList(patient: Patient) { // 인자로 참조
        println("Doctor: $name, Patient: ${patient.name}")
    }
}

fun main() {
    val doctor1 = Doctor("KimDoctor") // 객체는 서로 독립적으로 생성됨
    val patient1 = Patient("LeePatient")

    doctor1.patienList(patient1)
    patient1.doctorList(doctor1)
}
  • 의존(Dependency): 한 클래스가 다른 클래스에 의존되어 있어 영향을 주는 관계다.
/* 의존 관계 나타내기 */
class Patient(val name: String, var id: Int) {
    fun doctorList(doctor: Doctor) { // 인자로 참조
        println("Patient: $name, Doctor: ${doctor.name}")
    }
}

class Doctor(val name: String, val patient: Patient) {
    val customerId: Int = patient.id

    fun patienList() {
        println("Doctor: $name, Patient: ${patient.name}")
        println("Patient Id: $customerId")
    }
}

fun main() {
    val patient1 = Patient("LeePatient", 1)
    val doctor1 = Doctor("KimDoctor", patient1) // Patient 객체가 필요함

    doctor1.patienList()
}
  • 집합(Aggregation): 연관 관계와 거의 동일하나 특정 객체를 소유한다는 개념이 추가된 관계다.
/* 집합 관계 나타내기 */
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)
    }
}
  • 구성(Composition): 집합관계와 거의 동일하지만 특정 클래스가 어느 한 클래스의 부분이 되어 생명주기가 소유자 클래스에 의존되는 관계다.
/* 구성 관계 나타내기 */
class Car(val name: String, val power: String) {
    private var engin = Engine(power) // Engine 클래스 객체는 Car에 의존적

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

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

fun main() {
    val car = Car("tico", "100hp")
    car.startEngine()
    car.stopEngine()
}