231218 TIL #271 Kotlin #6 생성자 / 프로퍼티 / object 키워드

김춘복·2023년 12월 18일
0

TIL : Today I Learned

목록 보기
271/543
post-custom-banner

Today I Learned

Kotlin in Action 공부 이어서..


생성자와 프로퍼티

클래스 초기화

  • 자바에서는 생성자를 하나 이상 선언할 수 있다.
    코틀린에서도 같지만, 주(primary)생성자와 부(secondary)생성자를 구분한다.

주 생성자

클래스 이름 바로 뒤 소괄호()에서 설정한게 바로 주 생성자가 된다. val isMarried: Boolean=true 처럼 디폴트 값도 설정이 가능하다.

class Person(val name: String, val age: Int, val isMarried: Boolean=true) {
    fun printInfo() {
        println("Name: $name, Age: $age, Married: $isMarried")
    }
}

fun main() {
    val person = Person("Alice", 25)
    person.printInfo()
}
// 출력 : Name: Alice, Age: 25, Married: true
  • 클래스명 뒤에 () 소괄호 없이 클래스를 생성하면 주생성자 없이도 만들 수 있다.

  • 생성자의 매개변수는 기본적으로 생성자에서만 사용할 수 있는 지역변수로 클래스 안의 다른 함수에서는 다른 과정을 거치지 않으면 사용할 수 없다.
    간단하게 해결하려면 주 생성자의 매개변수의 앞에 val이나 var로 선언하면 클래스의 멤버 변수가 되어 다른 함수에서도 사용 가능하다.

  • 상속시 주 생성자
    기반 클래스가 있으면 주 생성자에서 기반 클래스의 생성자를 호출하는데, 기반 클래스의 이름 뒤에 괄호를 치고 생성자 인자를 넘겨준다.

// 기반 클래스
open class Person(val name: String, val age: Int) {
    // 주 생성자의 일부로부터 초기화 가능한 프로퍼티
}

// 상속받는 자식 클래스
class Student(name: String, age: Int, val studentId: String) : Person(name, age) {
    // 주 생성자에서 상속받은 프로퍼티와 추가된 프로퍼티
}
  • 비공개 생성자
    어떤 클래스를 클래스 외부에서 인스턴스화 하지 못하게 막고 싶으면 모든 생성자를 private으로 설정하면 된다. 아래처럼 주 생성자에 private을 붙이면 간단하다.
class Secretive private constructor(){}
  • init
    init 영역은 객체를 생성할 때 자동으로 실행된다. 꼭 선언할 필요는 없지만 주 생성자의 본문을 구현하고 싶을 때 주로 사용된다. (부 생성자로 객체를 생성할 때도 실행되지만 보조 생성자는 클래스 안에 선언하므로 {}를 통해 자체적인 본문을 구현할 수 있다)
class User(name: String, count: Int) {
	init {
    	println("I'm init)
    }
    // 본문..
}

부 생성자

일반적으로 코틀린에서는 디폴트 파라미터 값 설정 덕에 여러 생성자를 만들 일이 적다.
하지만 그럼에도 생성자를 여럿 만들어야 할 일이 있다.

  • 부 생성자는 constructor 키워드로 설정할 수 있다. 필요에 따라 얼마든지 생성 가능하다.
class Person(val name: String, val age: Int) {
    constructor(name: String) : this(name, 0) {...}
    
    constructor(name: String, age: Int, city: String) : this(name, age) {
    println("Person created in $city")
    //...
    }
    // ...
}
  • 주생성자와 부생성자가 모두 있을 경우, 반드시 주 생성자가 실행되어야 한다.
    부생성자에선 this를 키워드를 통해 주 생성자를 호출해서, 부 생성자의 매개변수를 주생성자의 매개변수와 연결시켜 주 생성자도 같이 실행시키는 과정을 거쳐야 한다.
    부생성자와 부생성자끼리 연결시킬수도 있다. 이 과정에서도 다른 부생성자를 통해 주 생성자는 실행되어야 한다.

  • 그리고 자바에서처럼 super 키워드를 통해 상위 클래스의 생성자를 호출할 수 있다.


인터페이스에 선언된 프로퍼티

코틀린에선 인터페이스에 추상 프로퍼티 선언을 넣을 수 있다.

interface User {
	val nickname: String
}
  • 인터페이스에 있는 프로퍼티 선언에는 뒷받침하는 필드나 게터 등의 정보가 들어있지 않다. 인터페이스에는 아무 상태도 포함할 수 없기 때문에 인터페이스를 구현한 하위 클래스에서 아래와 같은 방법으로 상태 저장을 위한 프로퍼티 등을 만들어야 한다.
// 1. 주 생성자 안에 프로퍼티를 직접 선언
class PrivateUser(override val nickname: String) : User

// 2. 커스텀 게터로 프로퍼티 설정. 필드에 저장하지 않고 매번 값을 계산한다.
class SubscribingUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore("@")
}

// 3. 초기화 식으로 nickname 초기화. 저장한 값을 불러온다.
class FacebookUser(val accountId: Int) : User{
    override val nickname = getFacebookName(accountId)
    private fun getFacebookName(accountId: Int): String {
        // ...
    }
}

데이터 클래스

자바에선 클래스가 equals, hashCode, toString 등의 메서드를 직접 구현해야 한다.(아니면 IDE가 자동으로 구현)
하지만 코틀린에서는 컴파일러가 이런 메서드를 기계적으로 생성해주는 기능이 있는데, 이를 보이지 않는 곳에서 해줘 소스코드를 깔끔하게 유지해준다.

  • 간단히 클래스 명 앞에 data 제어자를 붙이면 컴파일러가 자동으로 만들어주고, 이를 데이터 클래스라 한다.
data class Client(val name: String, val postalCode: Int)

fun main() {
    val client1 = Client("kim", 123)
    val client2 = Client("kim", 123)

    if (client1 == client2) println("Equal!") // Equal!
    println(client1.toString()) // Client(name=kim, postalCode=123)
    println(client1.hashCode()) // 3292040
    println(client2.hashCode()) // 3292040
}
  • 구현해주는 메서드 설명
    toString() : 클래스의 각 필드를 선언 순서대로 표시하는 문자열 표현 생성.
    equals() : 객체의 동등성. 인스턴스 비교를 위한 메서드.
    (코틀린에서는 ==를 참조타입 비교에도 쓸 수있다)
    hashCode() : 해시 컨테이너. 해시맵같은 해시기반 컨테이너에서 키로 사용가능.

copy()

  • data 클래스의 모든 프로퍼티가 val일 필요는 없지만 val로 읽기전용으로 만들어 데이터 클래스를 불변 클래스로 만드는 것을 권장한다.

  • copy() 메서드는 객체를 복사해 일부 프로퍼티를 바꿀 수 있게 해주는 메서드이다.
    이 메서드를 통해 var을 사용하는 대신 객체 자체를 카피해 프로퍼티를 변경해줄 수 있다.

    val client3 = client1.copy(postalCode = 456)
    println(client3) // Client(name=kim, postalCode=456)

클래스 위임 (by 키워드)

코틀린에선 기본적으로 클래스가 final로 상속을 허용하지 않는다.
이때 클래스에 새로운 동작을 추가하기 위해 일반적으로 데코레이터 패턴을 사용한다.
기존의 클래스(상속x) 대신 새로운 데코레이터를 만들되 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게하고, 기존 클래스를 데코레이터 내부에 필드로 유지하는 것이다.

  • 위의 데코레이터 방식은 준비 코드가 상당히 많이 필요하다는 단점이 있다.
    하지만 코틀린에선 인터페이스를 구현할 때 by 키워드를 통해 간단하게 구현을 다른 객체에 위임한다는 사실을 명시할 수 있다.

  • 즉, 상속할 수 없는 final 상태의 기본 클래스를 클래스 위임을 통해 해당 클래스의 기능을 재사용할 수 있는 기능이다.

  • class C : A by B : A에서 정의하는 모든 B의 메서드를 C에게 위임한다.
    즉, C는 B가 갖고있는 모든 A의 메서드를 구현하지 않아도 가질 수 있다.

  • 아래와 같이 위임 하면 Car 클래스는 엔진의 세부 구현을 몰라도 되며, 이미 만들어진 ElectricEngine 클래스를 재사용하여 엔진을 사용할 수 있다.

interface Engine {
    fun start()
}

class ElectricEngine : Engine { // 전기 엔진 클래스가 엔진을 구현
    override fun start() {
        println("전기 엔진 시작")
    }
}
// 차가 엔진 인터페이스를 전기엔진에게 위임받음
class Car(private val engine: Engine) : Engine by engine

fun main() {
    val electricEngine = ElectricEngine()
    val myCar = Car(electricEngine)
    myCar.start() // "전기 엔진 시작" 출력
}

object 키워드

객체 선언 : 싱글톤 패턴 구현

object MySingleton {
    fun doSomething() {
        println("It is Singleton Object")
    }
}
  • 위와 같이 object 로 객체 선언을 하면 클래스 선언 + 단일 인스턴스 선언이 되어 인스턴스가 하나인 싱글톤으로 사용할 수 있다.

  • 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어지므로 생성자를 쓸 수 없다.

  • 일반 클래스처럼 다른 클래스의 상속이나 인터페이스의 구현도 가능하다.

  • 자바에서 사용하려면 INSTANCE 필드를 사용하면 된다.
    ex) MySingleton.INSTANCE.doSomeThing();


동반 객체 (companion object)

  • 코틀린 클래스는 static을 지원하지 않아 정적인 멤버가 없다.
    대신 패키지 수준의 최상위 함수와 객체 선언을 활용하면 되는데, 대부분 최상위 함수를 사용한다.
    하지만 최상위 함수는 클래스의 private 멤버에 접근할 권한이 없다.

  • 동반 객체는 클래스 내부에 선언되며, 그 클래스와 연결된 싱글톤 객체를 나타낸다. 클래스의 인스턴스를 만들지 않고도 해당 클래스의 멤버에 접근하거나 메서드를 호출할 수 있는 기능을 제공함으로써 static 멤버를 가지는 것과 비슷한 기능을 한다.

  • 클래스 안에 companion object{...}로 선언하면 된다.

class MyClass {
    companion object {
        // 동반 객체의 멤버들
        fun myFunction() {
            println("This is a function in companion object")
        }
        val myProperty: Int = 42
    }
}
// 동반 객체의 멤버에 접근
MyClass.myFunction() // "This is a function in companion object"
println(MyClass.myProperty) // 42
  • 동반객체는 둘러싼 클래스의 private에 접근할 수 있어 최상위 함수와 차별점을 가진다.

  • 이로써 특정 클래스의 인스턴스를 생성하지 않아도 접근할 수 있기 때문에, 정적(static) 멤버와 유사한 효과를 얻을 수 있으면서도 객체 지향 프로그래밍의 특성을 유지할 수 있다.

  • 팩토리 메서드 패턴을 구현하기에 가장 적합한 방식이다.

  • 동반객체는 클래스 안에 정의된 일반객체로, 이름을 붙이거나 인터페이스를 구현하거나 안에 확장함수와 프로퍼티를 정의할 수도 있다.(이름을 안붙이면 Companion이라는 이름으로 자바에서 접근이 가능하다)

  • 클래스 안에 빈 동반객체를 선언해두고, 클래스 밖에서 Companion에 대한 확장함수를 선언해 클래스의 멤버함수처럼 사용할 수 있다.


객체식

함수 내부에서 간단한 객체를 정의하고 사용할 때 유용하다.
해당 함수 내에서만 유효한 임시 객체를 생성할 수 있다.

fun someFunction() {
    val obj = object {
        val x: Int = 10
        val y: Int = 20
    }
    println("x: ${obj.x}, y: ${obj.y}")
}

익명 객체

익명 객체는 주로 인터페이스를 구현하거나 클래스를 상속받을 때 사용된다.
한 번만 사용할 목적으로 만들어지며, 그 자체로는 재사용되지 않는다.

interface MyInterface {
    fun doSomething()
}

val myObject: MyInterface = object : MyInterface {
    override fun doSomething() {
        println("Anonymous Object is doing something")
    }
}
profile
Backend Dev / Data Engineer
post-custom-banner

0개의 댓글