상세 컨텐츠

본문 제목

5주차 - 클래스와 객체의 정의

책 리뷰/Do it! 코틀린 프로그래밍

by 근성 2024. 1. 6. 16:14

본문

이번 주차는 클래스와 객체이다.


클래스와 객체의 정의

객체지향 프로그래밍에서는 떼고 싶어도 뗄 수 없는 단어이다.

본 책에서는 단어를 객체에 관한 단어들을 정의해두었다.(나중에 다시 보자)

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

 

쉽게 생각하자, 어떤 클래스가 있다.

그 클래스에서 쓰는 변수를 프로퍼티, 함수를 메서드. 간단하다.

 

Bird라는 클래스를 만들어보자.

package chap05.section1

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 = "blue" // 객체의 프로퍼티에 값 할당

    println("coco.color: ${coco.color}") // 객체의 멤버 프로퍼티 읽기
    coco.fly() // 객체의 멤버 메서드 사용
    coco.sing(3)
}

 

바뀐 프로퍼티가 잘 출력된다.

 


생성자

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

클래스 안의 프로퍼티 값을 직접 입력하여 초기화해도 되지만 이렇게 하면 항상 같은 프로퍼티 값을 가지는 객체가 만들어진다.

객체를 생성할 때 필요한 값을 설정하여 객체를 만들면 훨씬 유연할 것이다.

생성자는 주 생성자와 부 생성자로 나뉘며 필요에 따라 주 생성자 또는 부 생성자를 사용할 수 있다.

 

부생성자

앞에서 나온 코드로 설명하겠다.

package chap05.section2

class Bird{ // 클래스 정의
    // 프로퍼티 선언만 함
    var name: String
    var wing: Int
    var beak: String
    var color: String

    // 부 생성자 - 매개변수를 통해 초기화할 프로퍼티에 지정
    constructor(name: String, wing: Int, beak: String, color: String){
        this.name = name // this.name은 선언된 현재 클래스의 프로퍼티를 나타냄
        this.wing = wing
        this.beak = beak
        this.color = color
    }

    // 메서드(함수)
    fun fly() = println("Fly wing: $wing")
    fun sing(vol: Int) = println("Sing vol: $vol")
}

fun main() {
    val coco = Bird("mybird", 2, "short", "blue") // 생성자의 인자로 객체 생성과 동시에 초기화
    coco.color = "yellow" // 객체의 프로퍼티에 값 할당

    println("coco.color: ${coco.color}") // 객체의 멤버 프로퍼티 읽기
    coco.fly() // 객체의 멤버 메서드 사용
    coco.sing(3)
}

프로퍼티로 타입만 지정하고, 부 생성자로 매개변수를 통해 프로퍼티에 지정.

그리고 메인함수에서 객체 생성과 동시에 초기화.

추상적이라고 말하지만 나한텐 너무 편하고 직관적이여서 마음에 들었다.

 

this 포인터를 사용하기 귀찮다면 언더바를 사용해도 괜찮다.

    // 부 생성자 - 매개변수를 통해 초기화할 프로퍼티에 지정
    constructor(_name: String, _wing: Int, _beak: String, _color: String){
        name = _name // this.name은 선언된 현재 클래스의 프로퍼티를 나타냄
        wing = _wing
        beak = _beak
        color = _color
    }

 

 

주생성자

package chap05.section2.primary

class Bird(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")
}

fun main() {
    val coco = Bird("mybird", 2, "short", "blue") // 생성자의 인자로 객체 생성과 동시에 초기화
    coco.color = "yellow" // 객체의 프로퍼티에 값 할당

    println("coco.color: ${coco.color}") // 객체의 멤버 프로퍼티 읽기
    coco.fly() // 객체의 멤버 메서드 사용
    coco.sing(3)
}

 

코드가 더욱 간략하게 줄어든다.

 

초기화 블록을 가진 코드도 있다.

package chap05.section2.init

class Bird(var name: String, var wing: Int, var beak: String, var color: String) {
    // 초기화 블록
    init {
        println("----------초기화 블록 시작----------")
        println("이름은 $name, 부리는 $beak")
        this.sing(3)
        println("---------- 초기화 블록 끝 ----------")
    }

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

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

    coco.color = "yellow" // 객체의 프로퍼티에 값 할당
    println("coco.color: ${coco.color}") // 객체의 멤버 프로퍼티 읽기
    coco.fly() // 객체의 멤버 메서드 사용
}

 


상속과 다형성

 

상속에 대해서 먼저 알아보자.

클래스는 자식 클래스를 만들 때 상위 클래스(부모 클래스)의 속성과 기능을 물려받아 계승하는데 이것을 상속이라고 한다.

package chap05.section3.openclass

// 상속 가능한 클래스를 선언하기 위해 Open 사용
open class Bird(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")
}

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

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")
}

fun main() {
    val coco = Bird("mybird", 2, "short", "blue")
    val lark = Lark("mylark", 2, "long", "brown")
    val parrot = Parrot("myparrot", 2, "short", "multiple", "korea") // 프로퍼티 추가

    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()

}

 

 

 

다형성

이름이 동일하지만 매개변수가 서로 다른 형태를 취하거나 실행 결과를 다르게 가질 수 있는 것을 다형성이라고 한다.

 

만약에 print()라는 함수가 있다면, 우리는 print("hello"), print(123) 등으로 쓴 경험이 있을것이다.

여기서 인자의 형식이 바꼈다는 차이가 있는데, 인자의 형식만 달라지는것을 오버로딩(Overloading)이라고 한다.

 

덧셈연산자의 오버로딩을 살펴보자.

add라는 함수는 많은데, 인자의 형식이 다른것을 확인할 수 있다.

package chap05.section3

fun main() {
    val calc = Calc()
    println(calc.add(3,2))
    println(calc.add(3.2,1.3))
    println(calc.add(3,2, 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 + z
    fun add(x: String, y: String) : String = x + y
}

 

상위와 하위 클래스에서 메서드나 프로퍼티의 이름은 같지만 기존의 동작을 다른 동작으로 재정의 하는것을 오버라이딩(Overriding)이라고 한다.

bird 예제로 다시 알아보자.

package chap05.section3

// 상속 가능한 클래스를 위헤 open 사용
open class Bird(var name: String, var wing: Int, var beak: String, var color: String){
    // 메서드
    fun fly() = println()
    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){
        fun speak() = println("Speak! $language") // Parrot에 추가된 메서드
        override fun sing(vol: Int){ // override된 메서드
            println("I'm a parrot! The volume level is $vol")
            speak() // 달라진 내

        }
    }

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

    println("Parrot: ${parrot.name}, ${parrot.wing}, ${parrot.beak}, ${parrot.color}, ${parrot.language}")
    parrot.sing(5) // 달라진 메서드 실행 가능
}

 


super와 this의 참조

this와 super를 통해 현재 객체를 사용하는 부 생성자이다.

package chap05.section4.personthis

open class Person{
    constructor(firstName: String){
        println("[Person] firstName: $firstName")
    }

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

    }

}

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

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

fun main() {
    val sean = Developer("Sean")

}

 

주 생성자와 부 생성자를 함께 사용하는 경우도 있다.

package chap05.section4.prisecon

class Person(firstName: String, out: Unit = println("[Primary Constructor] Parameter")){ // 2. 주 생성자
    val fName = println("[Property] Person fName: $firstName") // 3. 프로퍼티 할당

    init {
        println("[init] Person init block") // 4. 초기화 블럭
    }

    // 1. 부 생성자
    constructor(firstName: String, age: Int, out: Unit = println("[Secondary Constructor] Parameter"))
    : this(firstName){
        println("[Secondary Constructor] Body: $firstName, $age") // 5. 부 생성자 본문
    }

}

fun main() {
    val p1 = Person("Kildong", 30) // 1 -> 2 호출, 3 -> 4 -> 5 실행
    println()
    val p2 = Person("Dooly") // 2 호출, 3 -> 4 실행
}

 

내부 클래스(inner class)에서 바깥 클래스로 접근하는 방법이 있다.

super키워드와 @기호를 사용해야한다.

package chap05.section4.innerref

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() // 현재 이너 클래스의 f() 접근
            Child().f() // 바로 바깥 클래스 f() 접근
            super@Child.f() // Child의 상위 클래스인 Base클래스의 f() 접근
            println("[Inside] super@Child.x: ${super@Child.x}") // Base의 x 접근
        }
    }
}

fun main() {
    val c1 = Child()
    c1.Inside().test() // 이너 클래스 Inside의 메서드 test() 실행
}

 

인터페이스에서 참조할 수도 있는데, 

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

따라서 인터페이스 자체로는 객체로 만들 수 없고 항상 인터페이스를 구현하는 클래스에서 생성해야한다.

코틀린은 자바처럼 한 번에 2개 이상의 클래스를 상속받는 다중 상속이 되지 않는다.

하지만 인터페이스로는 필요한 만큼 다수의 인터페이스를 지정해 구현할 수 있다.

package chap05.section4

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

interface B{
    fun f() = println("B interface f()") // 인터페이스는 기본적으로 open임
    fun b() = println("B interface b()")
}

class C : A(), B{ // 쉼표(,)를 사용해 클래스와 인터페이스를 지정
    // 컴파일되려면 f()가 오버라이딩 되야함
    override fun f() = println("C Class f()")

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

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

정보 은닉 캡슐화

클래스를 작성할 때 숨겨야 하는 속성이나 기능이 있는데, 이런 개념을 캡슐화라고 한다.

아래의 키워드 종류를 살펴보자.

종류 설명
private 이 요소는 외부에서 접근할 수 없다.
public 이 요소는 어디서든 접근이 가능하다.(기본값)
protected 외부에서 접근할 수 없으나 하위 상속 요소에서는 가능하다.
internal 같은 정의의 모듈 내부에서는 접근이 가능하다.

 

각 예시를 살펴보자.

 

Private

package chap05.section5.privatetest

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() // 객체 생성 가능 
}

아래 코드인데 IntelliJ의 기능 덕분에 책의 내용을 스포당한기분이다.

 

Protected

아래 코드에서 main함수는 protect를 걸지 않는 access()함수만 접근 가능하다.

package chap05.section5.protectedtest

open class Base { // 최상위 클래스에는 protected를 사용할 수 없음
    protected var i = 1
    protected fun protectedFunc(){
        i += 1 // 접근 허용
    }
    fun access(){
        protectedFunc() // 접근 허용
    }

    protected class Nested // 내부 클래스에는 지시자 허용
}

class Derived : Base() {
    fun test(base: Base): Int{
        protectedFunc() // Base 클래스의 메서드 접근 가능
        return i // Base 클래스의 프로퍼티 접근 가능
    }
}

fun main() {
    val base = Base() // 생성 가능
    base.i // 접근 불가
    base.protectedFunc() // 접근 불가
    base.access() // 접근 가능
}

 

 

Internal

package chap05.section5.internal

internal class InternalClass {
    internal var i = 1
    internal fun icFunc() {
        i += 1 // 접근 허용
    }
    fun access(){
        icFunc() // 접근 허용
    }
}

class Other{
    internal val ic = InternalClass() // 프로퍼티를 지정할 때 internal로 맞춰야함
    fun test(){
        ic.i // 접근 허용
        ic.icFunc() // 접근 허용
    }
}

fun main(){
    val mic = InternalClass() // 생성 가능
    mic.i // 접근 허용
    mic.icFunc() // 접근 허용
}

위 코드를 외부 파일에서도 적용해보겠다.

package chap05.section5.internal

fun main(){
    val otheric = InternalClass()

    println(otheric.i)
    otheric.icFunc()
}

잘 접근이 되는것을 확인할 수 있다.

 

위에것들을 종합해서 자동차와 도둑의 예시로 코드를 정리했는데, 한번 실습해보자.

package chap05.section5.burglar

open class Car protected constructor(_year: Int, _model: String, _power: String, _wheel: String){
    private var year: Int = _year
    public var model: String = _model // public은 기본값이므로 생략 가능
    protected open var power: String = _power
    internal var wheel: String = _wheel

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

    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){
        override var power: String = "50hp"
    val driver = Driver(name, "first class")

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

    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}") // public
            driver.driving() // internal
        }
        else{
            println("You're a burglar")
        }
    }
}

class Burglar() {
    fun steal(anycar: Any){
        if(anycar is Tico){ // 인자가 Tico의 객체일 때
            println("----[Burglar] steal()---------")
            // println(anycar.power) // protected 접근 불가
            // println(anycar.year) // private는 접근 불가
            println("anycar.name = ${anycar.name}") // public wjqrms
            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("dontknow")
        }
        else{
            println("Nothing to steal")
        }
    }
}

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

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


클래스와 클래스의 관계

본 책에서는 클래스 간의 관계를 총 4가지로 정의했다.

연관, 의존, 집합, 구성 각각의 관계를 이해해보자.

 

연관 관계

2개의 서로 분리된 클래스가 단방향 혹은 양방향으로 연결되는것을 말한다.

package chap05.section6.association

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

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

fun main(){
    val doc1 = Doctor("Kimsabu") // 객체가 따로 생성됨
    val patient1 = Patient("Kildong")
    doc1.patientList(patient1)
    patient1.doctorList(doc1)
}

 

의존 관계

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

package chap05.section6.dependency

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 patientList(){
        println("Doctor: $name, Patient: ${p.name}")
        println("Patient Id: $customerId")
    }
}

fun main(){
    val patient1 = Patient("Kildong", 1234)
    val doc1 = Doctor("Kimsabu", patient1)
    doc1.patientList()
}

main함수에서 봤을때, Patient의 객체의 변수를 Doctor의 객체를 생성할때 사용했다.

 

집합관계

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

package chap05.section6

// 여러 마리의 오리를 위한 List 매개변수
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)
        }
    }

 

 

구성 관계

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

package chap05.section6.composition

class Car(val name: String, val power: String){
    private var engine = Engine(power) // Engine 클래스 객체는 Car에 의존적

    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()
}

 

관련글 더보기

댓글 영역