본문 바로가기

스터디/다재다능 코틀린 프로그래밍

7. 객체와 클래스

다재다능 코틀린 프로그래밍 책으로 코틀린 스터디를 진행하면서 발표를 위해 준비했던 글입니다.


객체와 싱글톤

  • 자바에서 싱글톤은 구현하기 까다로운 디자인 패턴이다, 방법도 여러가지고..
  • 코틀린에서는 싱글톤을 직접 지원한다. (역시 사기언어)
  • 클래스를 정의할 것인가 정의없이 객체를 생성할 것인가는 개발자의 선택이다.
    • ex) 추상클래스를 정의해야하는 복잡한 상황에선 클래스를 정의할 수 있다.

객체 표현식으로 사용하는 익명 객체

// 객채 표현식으로 원을 표현할 데이터를 가진 circle 익명 객체 생성
fun createCircle() {
    val circle = object {
        val x = 10;
        val y = 20;
        val radius = 30;
    }
    ...
}
  • 익명 객체에 메소드를 추가할 수도 있다. 근데 그럴바엔 그냥 클래스를 정의하자.
  • 대부분의 기초적인 객체 표현식은 지역변수를 그룹핑하고 의도를 표현할 때만 유용하다.
// 객체 표현식으로 인터페이스 구현체 만들기
}fun createRunnable(): Runnable {
    return object: Runnable {
        override fun run() { println("run") }
    }
}
createRunnable().run()
// 싱글 추상 메소드 인터페이스(자바에선 함수형 인터페이스)는 이렇게도 가능
fun createRunnable2(): Runnable = Runnable { println("run") }
createRunnable2().run()
// 둘 이상의 인터페이스 구현 -> 리턴 타입을 꼭 명시해줘야한다.
fun createRunnable3(): Runnable = object: Runnable, AutoCloseable {
    override fun run() { println("run") }
    override fun close() { println("close") }
}
createRunnable3().run()
createRunnable3().close() // 당연 ERROR

객체 선언을 이용한 싱글톤

  • object 키워드와 {} 블록 사이에 이름을 넣는다면 명령문 또는 선언으로 인식
  • 코틀린의 대표적인 싱글톤 Unit
  • 싱글톤은 객체 선언을 아래 예제와 같이 객체 선언을 사용해라.
// 객체 선언을 이용해서 만든 Util 싱글톤 객체
object Util {
    fun numberOfProcessors() = Runtime.getRuntime().availableProcessors()
}
println(Util.numberOfProcessors()) //16
  • 코틀린 컴파일러는 Util을 클래스로 취급하지 않아서 Util로는 객체를 생성할 수 없다.
  • 자바의 유틸 클래스라고 생각해라.
  • 코틀린에서는 내부적으로 싱글톤 객체를 Util 클래스의 static 인스턴스라고 표현
  • 그러나 @JvmStatic 을 사용하지 않으면 바이트코드에서는 static이 되지 않는다. 이 내용은 뒤에서 설명 예정
  • 당연히 뮤터블한 상태는 두면 안된다.

탑레벨 함수 vs 싱글톤

package chapter07.util

// 탑레벨 함수
fun unitsSupported() = listOf("A", "B")

// 싱글톤
object Temperature {
    fun add(c: Int) = c + c
}
package chapter07.use

import chapter07.util.Temperature as Singleton // 패키지명에 얼라이어스 사용
import chapter07.util.*

fun main() {
    println(unitsSupported())
    println(Singleton.add(3))
}
  • 사용할 함수들이 하이레벨이거나 일반적이거나 넓게 사용될 예정이라면 패키지 안에 직접 넣어 탑레벨 함수로 사용하자.
  • 함수들이 연관되어 있다면 싱글톤을 사용하는게 의도 표현에 좋다.
  • 함수들이 상태와 연관되어 있다면 클래스로 분리하자. 상태가 상수면 무관

클래스 생성

class Car(val yearOfMake: Int)
  • yearOfMake, Int형 읽기 전용 속성 생성
  • 생성자 자동 생성
  • getter 자동 생성

인스턴스 생성

val car = Car(2019)
println(car.yearOfMake) // 2019
  • new 키워드가 없다.

필드와 속성

class Car(val yearOfMake: Int, var color: String)

// 바이트코드의 일부
public final class Car {
    private final int yearOfMake;
    private java.lang.String color;
    public final int getYearOfMake();
    public final java.lang.String getColor();
    public final void setColor(java.lang.String);
    pulbic Car(int, java.lang.Stirng);
}
  • 코틀린에선 클래스에 필드가 없다. 속성이다.

필드와 속성의 차이: 필드는 클래스에 비공개로 유지되고 get 및 set 속성을 통해 액세스 속성은 클래스를 사용하는 것들에 의해 액세스되는 외부 방식에 영향을 미치지 않으면 서 필드를 변경할 수있는 추상화 수준을 제공

  • car.yearOfMake는 실제로는 car.getYearOfMake()를 호출한 것 → 코틀린은 캡슐화를 깨뜨리지 않는다.
  • val로 선언한 속성은 getter만 생성
  • var로 선언한 속성은 getter,setter 생성
  • 선언한 생성자 자동생성

속성 제어 변경

class Car(val yearOfMake: Int, theColor: String) {
    var fuelLevel = 100
    var color = theColor
    set(value) {
        if (value.isBlank()) {
            throw RuntimeException("no empty!")
        }
        field = value
    }
}

fun main() {
    val car = Car(2019, "RED")
    car.color = "" // 에러 발생
}
  • setter나 getter 중 하나만 작성하면 백킹 필드가 자동 생성된다. 개발자가 생성하는 것은 아니다.
  • 같은 이름의 속성과 파라미터를 사용하지말자. this를 사용하지 않으니 더 혼란스럽다.
  • 코틀린이 필드를 내부적으로 만들었기 때문에 코드에서는 getter나 setter에 있는 field 키워드를 통해서만 필드를 사용할 수 있다.

접근 제어자

  • 코틀린은 자바에서 제공하는 public, private, protected 와 더불어 internal 접근 제어자를 제공한다.
  • internal은 같은 모듈에 있는 모든 코드에서 속성이나 메소드에 접근 가능하게 해줌
  • internal은 바이트코드에 직접 나타나지 않고 네이밍 컨벤션을 이용해서 코틀린 컴파일러에 의해 다뤄지므로 실행시간 오버헤드가 전혀 없다.
  • getter의 적븐 권한은 속성의 접근 권한과 동일하지만 setter의 경우엔 아래 예제와 같이 개발자가 직접 설정해줄 수 있다.
var fuelLevel = 100
private set

초기화 코드

class Car(val yearOfMake: Int, theColor: String) {
    var fuelLevel = 100
        private set
    var color = theColor
        set(value) {
            if (value.isBlank()) {
                throw RuntimeException("no empty!")
            }
            field = value
        }

    init {
        if (yearOfMake < 2020) {
            fuelLevel = 90
        }
    }
}
  • 클래스는 0개 이상의 init 블록을 가질 수 있으며 top-down으로 순차적으로 실행된다.
  • init 블록을 속성 선언 아래에 위치시키면 어디에서든 속성과 파라미터를 사용할 수 있다.
  • 가급적 init 블록은 만들지 말고 만들더라도 1개만 만들어라. 생성자에서 최대한 아무런 작업도 안하는 것이 프로그램의 안정성과 퍼포먼스 측면 모두에서 장점이 크다.

보조 생성자

  • 주 생성자에 모든 파라미터가 디폴트값을 가지고있으면 주 생성자와 함께 아규먼트가 없는 기본 생성자를 생성해준다.
  • 보조 생성자는 주 생성자를 호출하거나 다른 보조 생성자를 호출해야만 한다.
  • 보조 생성자의 파라미터는 val, var를 사용할 수 없다.
  • 보조 생성자에서는 속성을 선언할 수 없다.
class Person(val first: String = "junseong", val last: String = "hong") {
    var fullTime = true
    var location: String = "-"

    constructor(first: String, last: String, fte: Boolean) : this(first, last) {
        fullTime = fte;
    }
    constructor(first: String, last: String, loc: String) : this(first, last, false) {
        location = loc
    }

    override fun toString() = "$first $last $fullTime $location"
}

fun main() {
    println(Person()) // 자동 생성된 기본 생성자 사용, junseong hong true -
    println(Person("Jane", "Doe")) // Jane Doe true -
    println(Person("Jane", "Doe", false)) // Jane Doe false -
    println(Person("Jane", "Doe", "home")) // Jane Doe false home
}

인라인 클래스

  • 프리미티브 타입을 Wrapping 클래스로 사용하면 의도가 명확해지고 안정성이 생기지만 객체 생성과 메모리 사용에 관한 오버해드와 함께 퍼포먼스가 나빠질 가능 성이 있다.
  • inline 클래스는 아직 실험적인 기능이지만 컴파일 시간에는 Wrapping 클래스의 장점을 취할 수 있고 실행 시간에는 프리미티브 타입으로 취급된다. → 바이트코드로 변화됐을 때 프리미티브 타입으로 변경
inline class SSN(val id: String)
fun receiveSSN(ssn: SSN) {
    println("$ssn")
}
  • 위 코드에서 receiveSSN 메소드는 컴파일하면 SSN이 아닌 String을 받도록 변경되지만 코틀린 컴파일러는 SSN 인스턴스를 사용했는지 검증한다.
  • inline 클래스 기능은 기본 클래스와 동일하지만 내부를 살펴보면 메소드는 프리미티브 타입을 받는 static 메소드가 inline 클래스로 둘러싸여있다.

컴패니언 객체와 클래스 멤버

  • 코틀린은 static 메소드를 만들 수 없다. 대신 컴패니언 객체를 사용할 수 있다.
  • 컴패니언 객체는 클래스 안에 정의한 싱글톤이다.
  • 컴패니언 객체는 인터페이스를 구현할 수도 있고 다른 클래스를 확장할 수도 있고 코드 재사용에도 유용하다.
  • 클래스 변수와 클래스 메소드를 컴패니언 객체에 몰아서 정의한 느낌
class Machine(val name: String) {
    fun checkIn() = checkedIn++
    fun checkOut() = checkedIn--
    companion object {
        var checkedIn = 0
        fun minimumBreak() = "15 minutes"
    }
}

Machine("Mater").checkIn()
println(Machine.minimumBreak())
println(Machine.checkedIn)

컴패니언 접근

// 컴패니언 객체 자체의 참조에 접근하기
var ref = Machine.Companion
  • 컴패니언 객체에 이름을 지정해주면 더 접근하기 수월하다.
class Machine(val name: String) {
    companion object MachineFactory{
            ...
    }
}
var ref = Machine.MachineFactory

팩토리로 사용하는 컴패니언

  • 컴패니언 객체를 Java의 static factory method 처럼 사용할 수 있다.
class Machine(val name: String) {
        ...
    companion object MachineFactory {
                ...
        fun withName(name: String): Machine {
            return Machine(name)
        }
    }
}

컴패니언 객체와 Static은 다르다.

  • 컴패니언 객체의 멤버에 접근하면 코틀린 컴파일러는 싱글톤 객체로 라우팅한다.
  • Java와의 상호 운용성 측면에서 문제를 야기할 수 있지만 @표기가 문제를 해결해준다. → 17-3 static 메소드 생성에서 배울 예정

제네릭 클래스 생성

class PriorityPari<T: Comparable<T>>(member1: T, member2: T) {
    val first: T
    val second: T
    init {
        if (member1 >= member2) {
            first = member1
            second = member2
        } else {
            first = member2
            second = member1
        }
    }
}
  • compareTo() 메소드를 직접 사용하는 대신 >= 연산자를 사용해서 compareTo 메소드를 사용했다.
    • 비교연산자 >, <, >=, <= 는 compareTo 호출로 컴파일 된다.

데이터 클래스

  • 데이터 클래스에선 val, var가 아닌 파라미터는 사용할 수 없다. 필요하면 바디 {}안에 속성이나 메소드를 추가할 수 있다.
  • 데이터 클래스에 equals(), hashCode(), toString(), copy() 메소드를 자동으로 만들어준다.
  • 주생성자에 의해 정의된 각 속성에 접근할 수 있게 해주는 component 메소드들도 제공하고 componentN()이라고 부른다
data class Task(val id: Int, val name: String)
val task1 = Task(1, "일기쓰기")
val task2 = task1.copy(id = 2) 
println(task1) // Task(id=1, name=일기쓰기)
println(task2) // Task(id=2, name=일기쓰기)
  • 자동으로 만들어준 toString()과 새로운 값으로 업데이트해서 복사해주는 copy 메소드를 확인하자.
  • copy 메소드는 프리미티브 참조에 대한 쉘로우 카피만 가능하다.
  • componentN 메소드는 구조분해를 위해 주로 사용한다.
val task1 = Task(1, "일기쓰기")
val (_, name) = task1
println(name)
  • js에서의 구조분해는 객체 속성 이름에 기반하는데 반해 코틀린은 주 생성자 파라미터 순서에 기반한다.
  • 파라미터 순서가 바뀐다면 심각한 영향을 끼친다.
    • 자동화 테스트를 강화하자!
    • 순서가 안바뀔 것 같은 data class에만 구조분해를 사용하자.
    • 혹시 모르니 생성자의 추가된 속성은 파라미터 맨 뒤에 위치시키자?(아닐지도..)