본문 바로가기

책 요약하기/Do it! 코틀린 프로그래밍

#05-1. 클래스와 객체, 생성자, 상속과 다형성 2021-04-25

05-1 클래스와 객체의 정의

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

객체 지향 프로그래밍(OOP : Object Oriented Programming)은 프로그램의 구조를 객체 간 상호작용으로서 표현하는 프로그래밍 방식입니다.

객체 지향 프로그래밍 특징
- 추상화(Abstraction): 특정 클래스를 만들 때 기본 형식을 규정하는 방법
- 인스턴스(Instance): 클래스로부터 생성한 객체
- 상속(Inheritance): 부모 클래스의 내용을 자식 클래스가 그대로 물려받음
- 다형성(Polymorphism): 하나의 이름으로 다양한 처리를 제공
- 캡슐화(Encapsulation): 내용을 숨기고 필요한 부분만 사용
- 메시지 전송(Message Sending): 객체 간에 주고받는 메시지
- 연관(Association): 클래스 간의 관계
코틀린에서 사용하는 용어 다른 언어에서 사용하는 용어
클래스(Class) 분류, 범주
프로퍼티(Property) 속성(Attribution), 변수(Variable), 필드(Field), 데이터(Data)
메서드(Method) 함수(Function), 동작(Operation), 행동(Behavior)
객체(Object) 인스턴스(Instance)

보통 자바에서 클래스에 포함된 기능을 나타내는 함수를 메서드(Method), 변수를 필드(Field)라고 합니다. 메서드나 필드는 클래스 내부에 정의되므로 클래스의 멤버 메서드, 멤버 필드라고도 합니다. 코틀린에서는 필드 대신에 프로퍼티(Property)라고 합니다. 이유는 변수 또는 필드에 내부적으로 접근 메서드가 포함되어 있기 때문입니다.

- 생성자와 초기화 블록: 객체가 생성될 때 자동 실행되는 메서드 또는 코드 블록
- 프로퍼티: 변수의 이름과 변수의 접근 함수가 포함된 형태
- 메서드: 일반적인 함수의 형태
- 중첩(Nested)클래스와 이너(Inner)클래스: 클래스 내부에 구성되는 클래스
- 객체 선언: 클래스 없이 접근할 수 있는 객체

 

클래스 다이어그램

클래스를 이해하기 쉽게 표현하려면 통합 모델링 언어(UML: Unified Modeling Language)를 사용합니다. UML은 객체 지향 프로그램 설계를 위한 다이어그램 표기법입니다.

다이어그램 기법 종류
- 클래스 다이어그램(Class Diagram): 클래스의 정의와 관계를 나타내는 다이어그램
- 시퀀스 다이어그램(Sequence Diagram): 시간의 개념을 통해 클래스에서 생성된 객체의 실행 흐름
- 유스 케이스 다이어그램(Use Case Diagram): 사용자 관점에서 사용 방법에 대해 설명
- 상태 머신 다이어그램(State-Machine Diagram): 시스템 관점에서 상태가 어떻게 변화하는지 나타냄
 Bird
- name: String
- wing: Int = 2
- beak: String
- color: String
+ fly(): Unit
+ sing(vol: Int): Unit

클래스 다이어그램으로 표현해보면 다음과 같습니다. 3개의 상자로 이루어져 있고 클래스 이름, 프로퍼티, 메서드를 손쉽게 파악할 수 있습니다. 때론 프로퍼티나 메서드 상자를 생략할 수도 있습니다. 프로퍼티나 메서드 앞에 +나 -와 같이 가시성을 나타내는 기호를 사용할 수 있고, -는 private +는 public을 의미합니다.

 

클래스와 추상화

목표로 하는 대상에 대해 필요한 만큼 속성과 동작을 정의하는 과정을 '추상화(Abstraction)'라고 합니다.
''라는 개념을 추상화해보면 새를 표현할 클래스의 이름이 필요하므로 새 클래스 'Bird'라고 이름을 짓고, 새를 관찰해 보니 새의 기본 동작은 '날기'와 '노래를 부르는 것'이라는 것을 관찰했습니다. 그러면 날기 동작은 fly()함수로, 노래하기 동작은 sing()함수로 정의 할 수 있습니다. 그리고 새의 특징으로는 이름, 날개, 부리, 색상이 있습니다. 이는 속성 변수 name, wing, beak, color로 정의합니다. 이렇게 다양한 새들의 일반적인 동작과 특징을 모아 클래스를 정의하는 과정이 새를 추상화하는 과정입니다.

 

Bird 클래스 만들기

fun main() {
    val coco = Bird() // 4. 클래스 생성자를 통한 객체 생성
    coco.color = "blue" // 5. 객체의 프로퍼티에 값 할당

    println("coco.color = ${coco.color}") // 6. 객체의 프로퍼트 값 읽기
    coco.fly() // 7. 객체의 멤버 메서드 사용
    coco.sing(3)
}
// Bird 클래스 만들어 보기
class Bird { // 1. 클래스 정의
    // 2. 프로퍼티 정의
    var name: String = "mybird"
    var wing: Int = 2
    var beak: String = "short"
    var color: String = "blue"

    // 3. 메서드 정의
    fun fly() = println("Fly wing: $wing")
    fun sing(vol: Int) = println("Sing vol: $vol")
}
/*
    결과 :
    coco.color = blue
    Fly wing: 2
    Sing vol: 3
*/

 

객체? 인스턴스?

클래스는 개념일 뿐 실제 메로리에 존재해 실행되는 것이 아닙니다. 새로 비유하자면 Bird라는 표현은 모든 새를 가리키는 개념일 뿐 실제 살아서 움직이는 것이 아닙니다. 따라서 클래스로부터 객체(Object)를 생성해야만 비로소 클래스라는 개념의 실체인 객체가 물리적인 메모리 영역에서 실행되고 이것을 구체화 또는 인스턴스화(Instantiate)되었다고 합니다.
보통 객체와 인스턴스는 동일한 용어로 취급받지만 좀 더 정확히 표현하면 특정 클래스로부터 만들어진 객체는 그 클래스의 인스턴스라고 부릅니다.  객체는 좀 더 포괄적인 용어로 특정 클래스가 아닌 모든 실체화된 것들을 가리킵니다.

 

05-2 생성자

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

class 클래스 이름 constructor(필요한 매개변수..) { // 주 생성자의 위치
	...
    constructor(필요한 매개변수..) { // 부 생성자의 위치
    // 프로퍼티의 초기화
    }
    [constructor(필요한 매개변수..) { ... }] // 추가 부 생성자
    ...
}

 

부 생성자

fun main() {
    // 4. 생성자의 인자로 객체 생성과 동시에 초기화
    val coco = Bird2("mybird2", 2, "short", "blue")

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

    println("-------------------------------------")

    // 두번째 부 생성자 호출
    val coco2 = Bird2("mybird2", "long")
    println("coco2.color = ${coco2.color}")
    coco2.fly()
    coco2.sing(3)
}


class Bird2 {
    // 1. 프로퍼티만 선언
    var name: String = "mybird"
    var wing: Int = 2
    var beak: String = "short"
    var color: String = "blue"

    // 2. 부 생성자 - 매개변수를 통해 초기화할 프로퍼티에 지정
    constructor(_name: String, _wing: Int, _beak: String, _color: String) {
        // 3. 객체 생성 시 프로퍼티에 매개변수 값 할당
        this.name = _name
        this.wing = _wing
        this.beak = _beak
        this.color = _color
    }

    // 5. 두번째 부 생성자
    constructor(_name: String, _beak: String) {
        // 3. 객체 생성 시 프로퍼티에 매개변수 값 할당
        this.name = _name
        this.wing = 2
        this.beak = _beak
        this.color = "grey"
    }

    // 메서드
    fun fly() = println("Fly wing: $wing")
    fun sing(vol: Int) = println("Sing vol: $vol")
}
/*
    결과 :
    coco.color = blue
    Fly wing: 2
    Sing vol: 3
-------------------------------------
    coco2.color = grey
    Fly wing: 2
    Sing vol: 3
*/

 

주 생성자

주 생성자는 클래스 이름과 함께 생성자 정의를 이용할 수 있는 기법입니다.

fun main() {
    // 4. 생성자의 인자로 객체 생성과 동시에 초기화
    val coco = Bird3("mybird3", 2, "short", "blue")

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

class Bird3(var name: String, var wing: Int, var beak: String, var color: String) {
    // 프로퍼티는 매개변수 안에 var를 사용해 프로퍼티로서 선언되어 본문에서 생략됨.

    // 메서드
    fun fly() = println("Fly wing: $wing")
    fun sing(vol: Int) = println("Sing vol: $vol")
}
/*
    결과 :
    coco.color = yellow
    Fly wing: 2
    Sing vol: 3
*/

 

초기화 블록을 가진 주 생성자

init{...} 초기화 블록을 클래스 선언부에 넣어 변수를 초기화하는 것 말고도 특정한 작업을 하도록 코드를 작성할 수 있습니다.

fun main() {
    // 2. 객체 생성과 함께 초기화 블록 수행
    val coco = Bird4("mybird4", 2, "short", "blue")

    coco.color = "yellow"
    println("coco.color = ${coco.color}")
    coco.fly()
}

class Bird4(var name: String, var wing: Int, var beak: String, var color: String) {
    // 1. 초기화 블록
    init {
        println("--------초기화 블록 시작--------")
        println("이름은 $name, 부리는 $beak")
        this.sing(3)
        println("--------초기화 블록 끝--------")
    }
    // 메서드
    fun fly() = println("Fly wing: $wing")
    fun sing(vol: Int) = println("Sing vol: $vol")
}
/*
    결과 :
    --------초기화 블록 시작--------
    이름은 mybird4, 부리는 short
    Sing vol: 3
    --------초기화 블록 끝--------
    coco.color = yellow
    Fly wing: 2
*/

 

프로퍼티 기본값 지정

// 프로퍼티의 기본값 지정
class Bird5(var name: String = "NONAME", var wing: Int = 2, var beak: String, var color: String) {
    // name과 wing의 값을 입력하지 않으면 default값이 할당됨.
}

 

05-3 상속과 다형성

클래스는 자식 클래스를 만들 때 상위 클래스의 속성과 기능을 물려받아 계승하는데 이것을 상속(Inheritance)이라고 합니다. 상속을 이용하면 하위 클래스는 일부러 상위 클래스의 모든 내용을 다시 만들지 않아도 됩니다. 다형성(Polymorphism)이란 메서드가 같은 이름을 사용하지만 구현 내용이 다르거나 매개변수가 달라서 하나의 이름으로 다양한 기능을 수행할 수 있는 개념입니다.

 

상속과 클래스의 계층

모든 클래스의 상위 클래스는 Any 클래스이며, 상위 클래스를 명시하지 않으면 Any 클래스를 상속받게 됩니다.
상위 클래스를 상속하려면 상위 클래스를 open이라는 키워드와 함께 선언해야 합니다.

자바의 기본 클래스
코틀린과 달리 자바에서는 기본적으로 선언하는 클래스가 상속 가능한 클래스입니다. 자바에서 상속할 수 없는 클래스로 선언하려면 final 키워드를 사용해야 합니다.

 

하위 클래스 선언하기

open class 기반 클래스 이름 { // 묵시적으로 Any로부터 상속됨, open으로 파생 가능
...
}

class 파생 클래스 이름 : 기반 클래스 이름() { // 기반 클래스로부터 상속됨, 최종 클래스로 파생 불가
}

 

파생 클래스 생성

fun main() {
    val coco = BirdBase("mybird", 2, "short", "blue")
    val lark = Lark("mylark", 2, "long", "brown")
    val parrot = OverrideParrot("myparror", 2, "short", "multiple", "korean")

    println("Coco: ${coco.name}, ${coco.wing}, ${coco.beak}, ${coco.color}")
    println("Lark: ${lark.name}, ${lark.wing}, ${lark.beak}, ${lark.color}")
    println("Parrot: ${parrot.name}, ${parrot.wing}, ${parrot.beak}, ${parrot.color}, ${parrot.language}")
    lark.singHitone()
    parrot.speak()
    lark.fly()
}

// 1. 상속 가능한 기반 클래스 선언
open class BirdBase(var name: String, var wing: Int, var beak: String, var color: String) {
    fun fly() = println("Fly wing: $wing")
    fun sing(vol: Int) = println("Sing vol: $vol")
}

// 2. 주 생성자를 사용한 상속
class Lark(
    name: String,
    wing: Int,
    beak: String,
    color: String
    ) : BirdBase(name, wing, beak, color)
{
    // 새로 추가된 메서드
    fun singHitone() = println("Happy Song!")
}

// 3. 부 생성자를 사용하는 상속
class Parrot : BirdBase {
    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")
}
/*
    결과 :
    Coco: mybird, 2, short, blue
    Lark: mylark, 2, long, brown
    Parrot: myparror, 2, short, multiple, korean
    Happy Song!
    Speak! korean
    Fly wing: 2
 */
 
 

 

다형성

이름이 동일하지만 매개변수가 서로 다른 형태를 취하거나 실행 결과를 다르게 가질 수 있는 것을 다형성(Polymorphism)이라고 합니다. 또한 2가지 형태가 존재합니다.
동작은 동일하지만 인자의 형식만 달라지는 '오버로딩(Overloading)', 메서드나 프로퍼티의 이름은 같지만 기존의 동작을 다른 동작으로 재정의하는 것을 '오버라이딩(Overriding)'이라고 합니다.

 

오버로딩

fun main() {
    val calc = Calc()
    println(calc.add(3, 2))
    println(calc.add(3.2, 1.3))
    println(calc.add(3, 3, 2))
    println(calc.add("Hello", "World"))
}

class Calc {
    // 다양한 매개변수로 오버로딩된 메서드
    fun add(x: Int, y: Int): Int = x + y
    fun add(x: Double, y: Double): Double = x + y
    fun add(x: Int, y: Int, z: Int): Int = x + y
    fun add(x: String, y: String): String = x + y
}
/*
    결과 :
    5
    4.5
    6
    HelloWorld
*/

(이렇듯 동일한 add()메서드를 호출하지만 인자에 따라 호출되는 메서드가 달라집니다:))

 

오버라이딩

오버라이딩하기 위해 기반 클래스에서는 open키워드, 파생 클래스에서는 override 키워드를 각각 사용합니다.

fun main() {
    val overrideParrot = OverrideParrot(name = "myparrot", beak = "short", color = "multiple")
    overrideParrot.language = "English"

    println("Parrot: ${overrideParrot.name}, ${overrideParrot.wing}, ${overrideParrot.beak}, ${overrideParrot.color}, " +
            "${overrideParrot.language}")
    overrideParrot.sing(5)
}

open class OverrideBaseBird(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 OverrideParrot(
    name: String,
    wing: Int = 2,
    beak: String,
    color: String,
    var language: String = "natural"
) : OverrideBaseBird(name, wing, beak, color) {
    fun speak() = println("Speak! $language")
    override fun sing(vol: Int) {
        println("I`m a parrot! The volume level is $vol")
        speak()
    }
}
/*
    결과 :
    Parrot: myparrot, 2, short, multiple, English
    I`m a parrot! The volume level is 5
    Speak! English
*/

기반 클래스에서 open키워드로 수식된 sing()메서드는 파생 클래스에서 오버라이딩 하였고, 만약 기능 변경이 싫어서 아예 오버라이딩을 막고자 한다면 override 키워드 앞에 final 키워드를 사용해 하위 클래스에서 재정의되는 것을 막을 수 있습니다.

open class Lark() : Bird() {
    final override fun sing() { /* 구현부를 새롭게 재정의 */ } // 하위 클래스에서 재정의를 막음

 

이상 마치겠습니다 :)