본문 바로가기
코틀린/개발자를 위한 코틀린 프로그래밍

[chapter07] 클래스 관계 등 추가사항 알아보기

by 측면삼각근 2023. 8. 27.
728x90
반응형

들어가기 전에

본 포스팅은 개발자를 위한 코틀린 프로그래밍의 chapter단위로 공부하고, 정리, 부족한 내용의 추가 학습내용을 정리하는 블로깅입니다.

이전 포스팅 ⬇️

[chapter06] 내장 자료형 알아보기

 

[chapter06] 내장 자료형 알아보기

들어가기 전에 본 포스팅은 개발자를 위한 코틀린 프로그래밍 의 chapter단위로 공부하고, 정리, 부족한 내용의 추가 학습내용을 정리하는 블로깅입니다. 이전 포스팅 ⬇️ [chapter05] 클래스 알아

messycode.tistory.com


01 클래스 연관관계 알아보기

1.1 클래스 관계

여러 클래스 간의 관계는 다음과 같다.

  • 상속관계(is a): 클래스를 상속해서 하나의 클래스처럼 사용한다.
  • 연관; 결합관계(has a): 클래스를 상속하지 않고 내부적인 속성에 객체를 만들어서 사용한다.
  • 결합관계(약한 has a): 연관관계를 구성하는 방식 중 클래스 간 주종관계없이 단순하게 사용하는 관계
  • 조합관계 (강한 has a): 연관관계를 구성하는 방식 중 클래스 간의 주종관계가 있어서 따로 분리해서 사용할 수 없는 관계
  • 의존관계(사용 has a): 필요한 클래스를 매개변수 등으로 받아 필요한 시점에 사용하는 관계

1.2 결합(Aggregation) 관계 (has a)

클래스 간 연관관계 중 약한 연관관계가 결합관계이다. 단순하게 사용하는 클래스에서 사용될 클래스의 객체를 속성으로 만들어 사용한다. 보통 주 생성자에 객체를 전달받아서 구성한다.
이 관계의 특징은 다른 클래스를 단순하게 사용할 뿐 두 클래스가 만든 객체가 동일한 생명주기일 필요는 없다.

예시. 학생과 대학 클래스에서 Address 클래스를 사용한다.
이러한 방식으로 특정 목적에 필요한 객체를 다른 클래스에서 가져다 사용하는 관계가 결합관계이다.

    class Address (val street: String, val city: String, val state: String, val zip: String){
        fun printAddress() {
            println("Street: $street City: $city State: $state Zip: $zip")
        }
    }

    class Student(val rollName: Int, val studentName: String, val address: Address) {
        fun printStudent() {
            println("Roll No: $rollName")
            println("Student Name: $studentName")
            address.printAddress()
        }
    }

    class College(val collegeName: String, val address: Address) {
        fun printCollege() {
            println("College Name: $collegeName")
            address.printAddress()
        }
    }

1.3 조합(Composition) 관계

두 클래스는 주종관계이고, 생명주기도 동일하다. 즉 두 클래스는 항상 같이 생성되고, 같이 소멸하는 구조일 경우에만 사용하는 관계이다.
특정 제품을 만들 때 항상 하나의 구조로만 사용하는 경우에 이런 관계를 구성할 수 있으나, 실제 비즈니스상에서는 상속관계와 마찬가지로 이런 의미의 구성은 별로 발생하지 않는다.

결합관계와의 차이는 주 객체가 소멸할 때 보조 객체도 삭제되어 같이 사라진다는 것이다.

    class Engine{
        fun start(){
            println("Engine started")
        }
        fun stop(){
            println("Engine stopped")
        }
    }

    open class Car()

    class Toyota: Car(){
        lateinit var engine: Engine
        fun drive(){
            engine = Engine()
            engine.start()
            println("Driving Toyota")
        }
    }

    val toyota = Toyota()
    toyota.drive()
    /*
    Engine started
    Driving Toyota
    */

1.4 의존(Dependency) 관계

클래스 간 명확한 관계가 있는 것이 아니라, 특정 메서드 등에 객체를 전달받아서 사용만 하는 관계이다. 특정 기능을 공개하지 않고, 이 객체를 사용해서 특정 결과를 처리하는 경우 많이 사용한다.

02 속성과 메서드 재정의

코틀린에서는 게터와 세터 메서드를 가진 속성을 변수 대신 사용한다.
변수처럼 속성을 정의하지만, 자동으로 게터와 세터가 생성된다.

2.1 속성 정의

코틀린은 속성을 일반 변수처럼 정의하고 사용한다. 속성으로 사용하려면 게터와 세터를 사용자 정의를 통해 재정의 할 수 있다.

최상위 속성 처리

속성에 필요한 게터와 세터 사용자 정의

val valInt: Int = 0
    get() {
        return field
    }

var varInt: Int = 0
    get() {
        return field
    }
    set(value) {
        field = value
    }

클래스 속성에 게터와 세터 처리

클래스 멤버 속성 내의 게터와 세터도 사용자가 재정의 할 수 있다.

class Kclass {
        val valattr: Int = 0
            get() {
                println("valattr getter")
                return field
            }
        var varattr: Int = 0
            get() {
                println("varattr getter")
                return field
            }
            set(value) {
                println("varattr setter")
                field = value
            }
    }

속성 메서드 세터를 비공개로 처리

특정 속성에 변경 제한을 처리하려면 속성 세터 메서드를 비공개로 처리할 수 있다. 아래와 같은 경우, 외부에서는 변경할 수 없으나, 내부에서는 변경이 가능하다.

class Counter {
        var count = 0
            private set

        fun increment() = ++count
    }

배킹필드 사용하지 않기

클래스의 속성은 기본으로 배킹필드(field)를 사용한다. 게터와 세터를 재정의할 때 배킹필드를 지정하지 않고 사용할 수 있다.
인터페이스 내의 속성과 확장 속성을 정의하려면 내부에 배킹필드가 없다. 그래서 속성을 재정의 할 때는 임의의 계산으로 값을 처리해야 한다.

아래 예시는 배킹 필드가 사용되지 않는다.

    class Thing(val name: String)

    class Container(private val maxCapacity: Int) {
        private val things = mutableListOf<Thing>()
        val capacity: Int
            get() = maxCapacity - things.size
        val isFull: Boolean
            get() = things.size == maxCapacity

        fun put(thing: Thing) {
            if (isFull) throw IllegalStateException("Container is full")
            things.add(thing)
        }
        fun take(): Thing {
            if (things.isEmpty()) throw IllegalStateException("Container is empty")
            return things.removeAt(things.lastIndex)
        }
        fun query(): List<Thing> = things.toList()
    }
}

2.2 연산자 오버로딩

operator예약어로 연산자를 오버로딩(operator overloading)할 수 있다.

  • 예약어 operator를  fun 앞에 정의
  • 연산자에 해당하는 메서드 이름을 정의한다.
  • 클래스 내부의 메서드나 확장함수로 정의할 수 있다.
  • 메서드나 함수의 오버로딩 규칙에 따라 매개변수 개수나 자료형이 다르면 여러 개를 정의할 수 있다.
class Amount(var total: Int, var balance: Int) {
        operator fun plus(amount: Amount): Amount = Amount(total + amount.total, balance + amount.balance)

        operator fun plus(scale: Int): Amount = Amount(total + scale, balance + scale)
        
        override fun toString(): String = "Amount(total=$total, balance=$balance)"
    }
확장함수도 연산자 오버로딩을 처리할 수 있다.

infix처리 규칙

연산자를 사용할 때는 보통 이항연산자로 처리하므로 두 항 사이에 연산자를 넣는다.
메서드를 호출할 때 점연산자는 연산자처럼 처리하는 방식이 편리할 수 있다.

  • 예약어 infix는 함수나 메서드를 정의할 때 가장 앞에 지정한다.
  • 확장함수나 메서드의 매개변수가 1개여야 한다.
  • 매개변수에 초깃값을 지정할 수 없다.
  • 점 연산자를 사용해서 함수나 메서드를 호출할 수 있다.
class Add(var x: Int = 0) {
        infix fun add(y: Int) = x + y // 메서드에 점연산 처리를 안하려면 infix 처리
        infix fun sub(y: Int) = x - y // infix처리는 매개변수가 하나만 가능
        infix operator fun times(y: Int) = x * y // 연산자 오버로딩도 가능
        infix fun divide(y: Int) = x / y
    }

    val a = Add(10)
    println(a add 5) // 15
    println(a sub 5) // 5
    println(a * 5) // 50

03 특정 자료를 다루는 클래스 알아보기

3.1 데이터 클래스

클래스는 행위 중심으로 데이터를 은닉해서 구성하지만, 클래스 간 전송 등을 하려면 데이터 즉 속성만을 가진 클래스가 필요하다.
이때 데이터 클래스를 정의해서 사용하면 편리하다.

    fun Any.dir(): Set<String> {
        return this.javaClass.kotlin.members.map { it.name }.toSet()
    }

    data class Person(val name: String)

    val person1 = Person("John")
    println(person1.dir()) // [name, component1, copy, equals, hashCode, toString]

    val person2 = Person("John")
    println(person1 == person2)
    // 데이터 클래스를 정의하고, 동일한 내용으로 객체를 만들면 equals()가 true를 반환한다.
    val person3 = person1.copy()
    println(person1 == person3) // true

데이터 클래스 주 생성자 이외의 속성 처리

데이터 클래스는 항상 주 생성자에 정의된 속성이 기준이다.
클래스를 상속하거나 클래스 내부에 속성을 지정할 때도 주 생성자에 정의된 속성 값이 같으면 동일한 객체로 본다. 그래서 항상 주 생성자에 속성을 정의해서 사용해야 한다.

open class Person(var gender: Int = 0)
    data class Student(val name: String, val age: Int) : Person(0)

    val a = Student("John", 32)
    val b = Student("John", 32)
    b.gender = 1
    println("$a, $b, a.gender: ${a.gender}, b.gender: ${b.gender}")
    // Student(name=John, age=32), Student(name=John, age=32), a.gender: 0, b.gender: 1
    println(a == b) // true
    
    // 복사할 때는 상속한 속성을 인지하지 못함
    println(b.copy().gender) // 0

3.2 이넘 클래스

  • 예약어 enum을 클래스 앞에 작성
  • 이넘 클래스를 정의할 때 생성할 객체를 모두 내부에 정의, 이 객체의 이름을 상수처럼 사용. 이름을 모두 대문자로 작성
  • 클래스 정의 시 속성 추가 가능.
    속성 추가 시 속성에 맞는 인자를 전달해서 만들어야 함
  • 이넘 클래스 내부에 동반객체를 작성할 수 있고 동반 객체로 객체 생성 등 다양한 요건을 작성할 수 있다.
  • 이넘 클래스 내부에 추상 메서드를 정의하면 모든 객체 내부에 메서드를 재정의한다.
  • 인터페이스를 상속받아 내부에 구현할 수 있다. 객체별로 다를 경우 각각 구현하고, 전체가 같을 경우 하나만 구현
fun Any.dir(): Set<String> {
    return this::class.members
        .map { it.name }
        .toSet()
}

enum class CardType {
    SILVER, GOLD, PLATINUM
}

fun main() {
    println(CardType.SILVER.ordinal) // 순서
    println(CardType.SILVER.name) // 이름
    println(CardType.SILVER.dir()) // 메소드 목록
    // [name, ordinal, clone, compareTo, describeConstable, equals, finalize, getDeclaringClass, hashCode, toString, valueOf, values, entries]
}

이넘 클래스에 속성 추가

이넘 클래스에도 필요한 속성을 추가할 수 있다.

enum class CardType(val color: String) {
    SILVER("gray"), GOLD("yellow"), PLATINUM("black")
}

fun main() {
    println(CardType.SILVER.ordinal) // 순서
    println(CardType.SILVER.name) // 이름
    println(CardType.SILVER.dir()) // 메소드 목록
    // [name, ordinal, clone, compareTo, describeConstable, equals, finalize, getDeclaringClass, hashCode, toString, valueOf, values, entries]

    println(CardType.SILVER.color) // gray
    println(CardType.valueOf("SILVER")) // SILVER
}

동반 객체 추가

동반 객체를 추가해서 특정 메서드를 클래스 이름으로 바로 접근할 수 있게 만든다.

enum class CarType(val color: String) {
    SILVER("Silver"),
    GOLD("Gold"),
    PLATINUM("Black");

    companion object {
        fun getCardTypeByName(name: String): CarType {
            return valueOf(name.uppercase())
        }
        fun getIterator(): Iterator<CarType> {
            return values().iterator()
        }
    }

이넘 객체 내부 속성과 내장함수 처리

이넘 클래스 객체의 속성을 확인하고 내장 함수로 이넘 객체를 처리하는 방법

enum class JobType(val korName: String) {
    PROGRAMMER("프로그래머"),
    DESIGNER("디자이너"),
    MANAGER("매니저")
}

fun main() {
    val programmer = JobType.PROGRAMMER
    println(programmer.korName) // 프로그래머
    println(programmer.name) // PROGRAMMER
    println(programmer.ordinal) // 0

    // enum class의 values() 메서드를 사용하면 enum class의 모든 값을 배열로 반환한다.
    JobType.values().forEach { println("${it.name} = ${it.korName}") }
    /*
        PROGRAMMER = 프로그래머
        DESIGNER = 디자이너
        MANAGER = 매니저
    * */
    // 위와 동일한 코드. 이넘 객체 안을 읽어서 배열로 처리
    enumValues<JobType>().forEach { println("${it.name} = ${it.korName}") }

    println(enumValueOf<JobType>(JobType.PROGRAMMER.name)) // PROGRAMMER
    println(JobType.valueOf("PROGRAMMER")) // PROGRAMMER
}

인터페이스 추가

이넘 클래스에 인터페이스를 상속해서 구현할 수 있다. 상속한 메서드는 객체별로 재정의할 수도 있고, 공통 메서드는 클래스 내에서도 재정의가 가능하다.

interface Calculable {
    fun calculate(grade: Int): Int
    fun getTypes(): List<String>
}

enum class JobType(val korName: String) : Calculable {
    PROJECT_MANAGER("피엠") {
        override fun calculate(grade: Int): Int {
            return grade * 1000
        }
    },SOFTWARE_ENGINEER("소프트웨어 엔지니어") {
        override fun calculate(grade: Int): Int {
            return grade * 2000
        }
    },DESIGNER("디자이너") {
        override fun calculate(grade: Int): Int {
            return grade * 3000
        }
    };
    override fun getTypes(): List<String> {
        return enumValues<JobType>().map { it.name }
    }
}


fun main() {
    println(JobType.DESIGNER.calculate(10)) // 30000
    println(JobType.DESIGNER.getTypes()) // [PROJECT_MANAGER, SOFTWARE_ENGINEER, DESIGNER]
}

3.3 인라인 클래스(inline class)

기본 자료형과 구별해서 새로운 자료형이 필요할 경우 인라인 클래스를 만든다.

컴파일 타임은 별도의 자료형이지만, 인라인 특성상 소스 코드에 삽입되면 내부 속성에 지정된 자료형으로 처리된다.

  • 예약어 inline이나 @jvmInline을 사용한다. @jvmInline을 사용할 때는 예약어 value를 클래스 앞에 작성한다.
  • 주 생성자의 기본 속성은 하나만 작성한다.
  • 클래스 내부에 Init 블록, 속성, 메서드를 추가할 수 있다.
  • 클래스 내부에 작성된 속성은 배킹필드가 없으므로 get메서드로 초기화 처리한다.
  • 인터페이스도 상속해서 내부 추상 메서드를 구현할 수 있다.
inline class Password(val value: String) // 인라인 클래스 만들기

@JvmInline // 어노테이션으로 인라인 클래스 만들기
value class User(val value: String) // inline 키워드를 사용하지 않아도 된다.

fun main() {
    var password = Password("123456")
    println(password)
    println(password.javaClass.kotlin)
    var user = User("123456")

}

@JvmInline
value class Name(val s: String){
    init {
        require(s.length >= 4) { "Name must be at least 4 characters long" }
    }
    
    val length: Int
        get() = s.length

    fun greet() {
        println("Hello, $s")
    }
}
반응형