kotlin

코틀린 이모저모 - 2

e4g3r 2025. 4. 11. 15:33

Class

class Person { /*...*/ }

 

코틀린은 class 키워드를 통해 클래스를 정의할 수 있습니다.

class Person constructor(firstName: String) { /*...*/ } // OK

class Person(firstName: String) { /*...*/ } // OK

 

클래스 선언부에 constructor 키워드를 사용하여 기본 생성자를 정의할 수 있습니다.

기본 생성자가 가시성 수성자 private, protected, internal을 필요로 하지 않는다면 constructor 키워드는 생략 가능합니다.

(Java - 접근 제어자)

class Car(color: String, weight: Int, speed: Int, storage: Int) {
    val color = color // 첫번째 순서
    var weight = weight // 두번째 순서

    init {
        this.weight += 100 // 세번째 순서
    }

    val speed = speed // 네번째 순서
    val storage = storage // 다섯번째 순서
}

 

클래스에서 사용 할 프로퍼티를 정의할 때에는 body에서 처리할 수 있습니다.

또한 body에서는 init이라는 키워드를 사용할 수 있으며 인스턴스를 초기화하는 과정에서 추가 작업을 진행할 수 있습니다.

프로퍼티 초기화 과정 및 init 키워드는 body에 작성된 순서대로 진행됩니다.

class Car(val color: String, var weight: Int, val speed: Int, val storage: Int) {
    init {
        weight += 100
    }
}

fun main() {
    val car = Car("red", 1000, 230, 100)
    println(car.weight)
}

 

기본 생성자에서 val, var 키워드를 사용하면 자동적으로 클래스 프로퍼티로 추가되며 생성자에 전달 된 값으로 초기화가 진행됩니다.

따라서 위 코드처럼 간단해질 수 있습니다.

class Car(val color: String, var weight: Int, val speed: Int, val storage: Int) {
    init {
        weight += 100
        println("init")
    }

    constructor(color: String, weight: Int, speed: Int) : this(color, weight, speed, 0) {
    	println("create no storage car")
    }
}

fun main() {
    val noStorageCar = Car("red", 1000, 230)
    println(noStorageCar.storage)
}

// init
// create no storage car

 

기본 생성자 이외에 추가적인 보조 생성자도 생성이 가능합니다. 예시로 매개변수로 storage를 받지 않는 생성자를 만들었습니다.

보조 생성자를 사용하는 경우 최종적으로 기본 생성자를 이용해 객체를 생성하도록 위임해야 합니다.

 

보조 생성자와 init 키워드를 사용할 때 주의해야 할 점은 보조 생성자가 가장 먼저 하는 것은 기본 생성자의 호출(인스턴스 초기화)이기

때문에 인스턴스가 초기화 되는 과정과 함께 init 키워드가 호출된 이후 마지막으로 보조 생성자의 body가 수행된다는 것입니다.

상속

open class Car(
    val name: String
)

class SportsCar(name: String) : Car(name) {
}

class SportsCar2 : Car {
    constructor(name: String) : super(name)
}

 

코틀린의 클래스는 기본적으로 불변이기 때문에 상속 가능하도록 하기 위해서는 open 키워드를 사용해야합니다.

 

상속을 하는 경우 자식 클래스에 기본 생성자가 있다면 기본 생성자의 매개변수를 통해 부모 클래스를 초기화 시켜줄 수 있고

자식 클래스가 기본 생성자를 가지고 있지 않다면 보조 생성자를 통해 super 키워드를 이용해 부모 클래스를 초기화 해줘야합니다.

메서드 오버라이딩

open class Shape {
    open fun draw() { /*...*/ }
    fun fill() { /*...*/ }
}

class Circle() : Shape() {
    override fun draw() { /*...*/ }
}

 

메서드 오버라이딩의 경우 재정의 될 메서드는 상위 클래스에서 open 키워드로 지정이 되어야 합니다.

또한 재정의를 진행하는 하위 클래스에서는 override 키워드를 필수로 사용해야합니다.

둘 중 하나라도 만족되지 않으면 컴파일 오류가 발생합니다.

프로퍼티 오버라이딩

open class Shape {
    open val vertexCount: Int = 0
}

class Rectangle : Shape() {
    override val vertexCount = 4
}

 

클래스의 프로퍼티도 오버라이딩 될 수 있습니다.

open class Animal {
    open val type: Any = "Unknown"
    open var sound: String = "None"
}

class Dog : Animal() {
    // val 속성은 더 구체적인 타입으로 오버라이딩 가능
    override val type: String = "Canine"  // Any -> String (OK)
    
    // var 속성은 타입 변경 불가
    // override var sound: Any = "Woof"  // String -> Any (컴파일 에러)
    override var sound: String = "Woof"  // 동일한 타입 (OK)
}

 

Any -> String처럼 구체 타입으로 변경되는 경우 타입이 변경될 수도 있습니다.

 

그리고 val -> var은 가능하지만 var -> val은 불가능합니다.

val -> var의 경우 오버라이딩하면서 set 메서드를 추가하면 되지만 var -> val의 경우 불변으로 변경되기 때문에

set 메서드가 삭제되어야하기 때문입니다. (자식이 부모의 모든 기능을 제공해야한다는 것에 위배)

파생 클래스 초기화 순서

open class Parent {
    // 문제가 될 수 있는 코드
    init {
        // 이 메서드는 자식 클래스에서 오버라이드 될 수 있음
        doSomething()  // 위험!
    }

    open fun doSomething() {
        println("Parent's implementation")
    }
}

class Child : Parent() {
    // 이 속성은 Parent의 init 블록 실행 시점에는 초기화되지 않음
    val childProperty = "Child's property"

    override fun doSomething() {
        // 부모 생성자 실행 중에 이 메서드가 호출되면 childProperty는 아직 초기화되지 않음
        println("Child's implementation using: $childProperty")
    }
}

fun main() {
    val child = Child()
}

// Child's implementation using: null

 

위 예제에서 Child 생성자를 호출하게 되면 부모 클래스의 초기화를 위해 Parent의 생성자가 호출됩니다.

생성자 호출 이후 Parent의 init 블록이 실행되고 doSomething 메서드를 호출합니다.

 

이 때 호출되는 doSomething는 Parent의 doSomething가 아닌 Child의 doSomething 메서드가 호출됩니다.

(Child 생성자를 호출하여 진행되고 있는 과정이기 때문에 Child 타입의 doSomething을 바라봄 - 동적 바인딩)

하지만 아직 Child의 초기화 과정은 진행되지 않았기에 childProperty는 초기화 되지 않았고 결과적으로 null이 포함되어 출력됩니다.

 

코틀린 공식 문서에서는 이러한 초기화 과정 순서를 고려해야하므로 open 멤버를 생성자, 프로퍼티 초기화, init 블록에서 사용하지 말라고 권장합니다.

부모 클래스 호출

class FilledRectangle: Rectangle() {
    override fun draw() {
        val filler = Filler()
        filler.drawAndFill()
    }

    inner class Filler {
        fun fill() { println("Filling") }
        fun drawAndFill() {
            super@FilledRectangle.draw() // Calls Rectangle's implementation of draw()
            fill()
            println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // Uses Rectangle's implementation of borderColor's get()
        }
    }
}

 

Java와 마찬가지로 super 키워드를 통해 오버라이딩 메서드 속에서 부모 메서드를 호출 할 수 있습니다.

코틀린의 신비한 기능은 inner class에서 @OuterClass를 통해 outer의 super를 호출하는 기능입니다.

멤버가 겹치는 경우

open class Rectangle {
    open fun draw() { /* ... */ }
}

interface Polygon {
    fun draw() { /* ... */ } // interface members are 'open' by default
}

class Square() : Rectangle(), Polygon {
    // The compiler requires draw() to be overridden:
    override fun draw() {
        super<Rectangle>.draw() // call to Rectangle.draw()
        super<Polygon>.draw() // call to Polygon.draw()
    }
}

 

위 예제의 경우 Rectangle, Polygon에 모두 draw 메서드가 존재합니다.

 

따라서 Rectangle을 상속함과 동시에 Polygon을 구현하는 Square 클래스의 경우 이미 구현 자체는 되어있지만 모호함을 없애기 위해

draw 메서드를 한번 더 오버라이딩 해야 합니다.

프로퍼티

포스팅에서 종종 프로퍼티라는 것이 언급되고 짧게 스쳐 지나갔었습니다.

코틀린 공식문서를 읽어보면서 프로퍼티라는 개념이 흔한 변수와 좀 모호한 것 같아 계속 읽어보며 최대한 구별해보려고 노력했습니다.


제일 큰 차이로 프로퍼티는 setter / getter를 통해 값을 저장 / 조회 합니다. 반면에 변수는 값을 직접 대입하고 조회합니다.

class Address {
    var name: String = "Holmes, Sherlock"
    var street: String = "Baker"
    var city: String = "London"
    var state: String? = null
    var zip: String = "123456"
}

 

주로 프로퍼티는 클래스에서 사용됩니다. Java로 치면 인스턴스 멤버라고 볼 수 있습니다.

Address 클래스 body에 선언 된 name, street, city, state, zip 모두 클래스의 프로퍼티가 됩니다.

별도로 정의하지 않아도 내부적으로 각 프로퍼티는 getter, setter가 생성됩니다.

 

코틀린에서 val은 불변이고 var는 변할 수 있는 데이터입니다. 따라서 val의 경우 내부적으로 getter만 생성되고

var의 경우 getter, setter 둘 다 생성됩니다.

class Address {
    var name: String = "Holmes, Sherlock"
        get() {
            println("request get name")
            return field // backing field
        }
        set(value) {
            println("request set name")
            field = value // backing field
        }
}

fun main() {
    val address = Address()
    address.name = "E4ger"
    println(address.name)
}

// request set name
// request get name
// E4ger

 

프로퍼티의 getter, setter은 커스텀이 가능합니다.

Address의 name은 클래스 프로퍼티이기 때문에 main 함수에서 address.name으로 접근하면 내부적으로 get이 호출됩니다.

Address 클래스 내부에서 get을 커스텀하여 로그가 남도록 하였기에 request set name이 출력됩니다. name의 set 또한 동일합니다.

 

코드를 자세히 보면 get, set 내부에서 field라는 키워드가 사용되었는데요.

외부에서 Address의 name을 조회하거나 수정하려면 Address.name 이런식으로 접근합니다.

그런데 만약 get, set 내부에서 name에 접근하기 위해 name을 호출하게 되면 자기 자신을 호출하는 꼴이 됩니다. (무한 재귀)

 

따라서 코틀린은 get, set 내부에서는 field라는 키워드를 통해 프로퍼티의 값을 조회, 수정할 수 있도록 합니다.

이러한 개념을 Backing Field라고 정의합니다.

지연 초기화 프로퍼티, 변수

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // dereference directly
    }
}

 

코틀린에서 var 키워드가 사용 된 프로퍼티, 변수는 클래스가 생성될 때 즉시 초기화 되지 않을 수 있습니다.

위 코드에서 subject 프로퍼티는 lateinit 키워드가 사용되었고 생성자에서 초기화를 해주지 않습니다.

대신 setup 메서드에서 초기화를 해줍니다. 이러한 지연 초기화는 일반적으로 의존성 주입을 통해 프로퍼티를 초기화하거나
단위 테스트의 설정 단계에서 활용될 수 있습니다.

인터페이스 프로퍼티

interface MyInterface {
    val prop: Int // abstract

    fun foo() {
        print(prop)
    }
}

class Child : MyInterface {
    override val prop: Int = 29
}

 

코틀린의 인터페이스는 프로퍼티를 선언해서 구현체에게 특정 프로퍼티를 강제하도록 할 수 있습니다.

위 예제의 경우 MyInterface를 구현하는 구현체가 prop을 override 해야합니다.

인터페이스 오버라이딩 충돌

interface A {
    fun foo() { print("A") }
    fun bar()
}

interface B {
    fun foo() { print("B") }
    fun bar() { print("bar") }
}

class D : A, B {
    override fun foo() {
        super<A>.foo()
        super<B>.foo()
    }

    override fun bar() {
        super<B>.bar()
    }
}

 

구현체 D의 경우 A,B 인터페이스의 구현체입니다. 하지만 A,B 모두 foo, bar 메서드가 있기에 충돌이 발생합니다.

따라서 foo, bar 모두 오버라이딩을 통해 어떻게 처리될 건지 구현해야합니다.

함수형 인터페이스

fun interface IntPredicate {
   fun accept(i: Int): Boolean
}

// Creating an instance using lambda
val isEven = IntPredicate { it % 2 == 0 }

 

코틀린에서 함수형 인터페이스 선언은 fun interface 키워드를 통해 선언할 수 있습니다.

또한 함수형 인터페이스의 구현은 직접 클래스를 만드는 방법도 있지만 람다식으로 간편하게 처리할 수 있습니다.

fun interface Printer {
    fun print(text: String)
}

fun beautifulPrint(text: String) {
    println("beautiful $text")
}

fun main() {
    val printer = Printer(::beautifulPrint) // use callable references
    printer.print("abcdefg")
}

// beautiful abcdefg

 

함수형 인터페이스의 경우 암시적 생성자라는 것이 내부적으로 생성됩니다. 또한 암시적 생성자는 callable references가 지원됩니다.

 

따라서 함수형 인터페이스를 정의할 때 main 함수에 작성된 코드처럼 :: 연산자를 통해 Printer에서 구현해야 할 메서드를 정의해줄 수

있습니다. 위 코드에서는 beautifulPrint 메서드가 참조로 전달되어 Printer의 print 메서드 구현으로 정의됩니다.

'kotlin' 카테고리의 다른 글

코틀린 이모저모 - 4  (0) 2025.04.16
코틀린 이모저모 - 3  (0) 2025.04.12
코틀린 이모저모 - 1  (0) 2025.04.10