-
[코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.4 객체)코틀린 공부/코틀린 2023. 12. 30. 22:27
4.4 객체
코틀린에서 객체 선언은 클래스와 상수를 합한 것이며 객체 선언을 통해 싱글턴, 즉 인스턴스가 단 하나만 존재하는 클래스를 만들 수 있다. 그리고 자바 익명 클래스와 비슷한 역할을 하는 객체 식도 살펴본다.
4.4.1 객체 선언
코틀린은 어떤 클래스에 인스턴스가 오직 하나만 존재하게 보장하는 싱글턴 패턴을 내장하고 있다. 코틀린에서는 클래스와 비슷한 방법으로 싱글턴을 선언한다. 다만 class 대신 object라는 키워드를 사용한다.
object Application { val name = "My Application" override fun toString() = name fun exit() {} }
이런 객체 선언은 클래스를 정의하는 동시에 클래스의 인스턴스를 정의하는 것이기도 하다.
fun describe(app: Application) = app.name // Application은 타입임 fun main() { println(Application) // Application은 값임 }
객체 정의는 스레드 안전하다. 컴파일러는 실행되는 여러 스레드에서 싱글턴에 접근하더라도 오직 한 인스턴스만 공유되고 초기화 코드도 단 한 번만 실행디도록 보장한다.
객체 특징:
- 초기화는 싱글턴 클래스가 실제 로딩되는 시점까지 지연된다. 보통은 프로그램이 객체 인스턴스에 처음 접근할 때 초기화가 이뤄진다
- 객체에는 주생성자나 부생성자가 없다. 객체 인스턴스는 항상 암시적으로 만들어지기 때문에 객체의 경우 생성자 호출이 아무런 의미가 없다.
- 객체의 본문에 들어있는 클래스에는 inner가 붙을 수 없다.
- 객체의 모든 맴버가 필요할 때 import문을 아래처럼 사용 할 수 없다.
import Application.*
그 이유는 객체 안에 toString()이나 equals() 같은 공통 메서드가 존재하고, 이런 것들까지 임포트돼 문제가 생길 수 있다.
- 다른 클래스 안에 내포될 수 있다.
- 다른 객체 안에 내포될 수 있다. (이런 객체도 싱글턴 &인스턴스가 단 하나만 생김)
- 함수 내부나 지역 클래스 내부에는 넣을 수 없다. (외부 문맥에 의존하므로 싱글턴이 될 수 없기 때문)
4.4.2 동반 객체
내포된 클래스와 마찬가지로 내포 객체도 인스턴스가 생기면 자신을 둘러싼 클래스의 비공개 맴버에 접근할 수 있다.
이런 특성은 팩토리 디자인 패턴을 쉽게 구현하는 경우 유용하게 활용할 수 있다.
생성자를 직접 사용하고 싶지 않을 때가 있다.
예를 들어 생성자를 사용하면 어떤 사전 검사 결과에 따라 널을 반환하거나 다른 타입의 객체를 반환할 수 없다.
생성자는 항상 자신이 정의된 클래스의 객체를 반환하거나 예외를 던질 수 만 있기 때문이다.
이를 해결하는 방법은 생성자가 비공개로 지정해 클래스 외부에서 사용할 수 없게 한 다음, 내포된 객체에 팩토리 메서드 역할을 하는 함수를 정의하고 그 함수 안에서 필요에 따라 객체의 생성자를 호출하는 것이다.
class Application private constructor(val name: String){ object Factory{ fun create(args: Array<String>): Application? { val name = args.firstOrNull() ?: return null return Application(name) } } }
fun main() { val name = arrayOf("hi") val app = Application.Factory.create(name) ?: return println("Application started: ${app.name}") }
이런 경우 별도의 import Application.Factory.create로 팩토리 메서드를 임포트하지 않는 한 매번 내포된 객체의 이름을 지정해야 한다.
코틀린에서는 Factory 매서드를 동반 객체(companion object)로 정의함으로써 이런 문제를 해결할 수 있다. 동반 객체는 companion이라는 키워드를 사용하는 내포된 객체다.
동반 객체의 맴버에 접근할 때는 동반 객체의 이름을 사용하지 않고 동반 객체가 들어있는 외부 클래스의 이름을 사용할 수 있다.
class Application private constructor(val name: String){ companion object Factory{ fun create(args: Array<String>): Application? { val name = args.firstOrNull() ?: return null return Application(name) } } }
val app = Application.create(name) ?: return
위에 있는 예시에서 Factory라는 이름을 아예 생략할 수도 있다. 그리고 이런 방식을 더 권장한다.
class Application private constructor(val name: String){ companion object { fun create(args: Array<String>): Application? { val name = args.firstOrNull() ?: return null return Application(name) } } }
동반 객체의 이름을 생략한 경우 컴파일러는 동반 객체의 디폴트 이름을 Companion으로 가정한다.
import Application.Companion.create
클래스에 동반 객체가 둘 이상 있을 수는 없다.
companion 변경자를 최상위 객체 앞에 붙이거나 다른 객체에 내포된 객체 앞에 붙이는 것은 금지된다. 최상위 객체의 경우 동반 객체를 연결할 클래스 정의가 없기 때문이고, 객체에 내포된 객체의 경우 companion을 붙이는 것이 불필요한 중복이기 때문이다.
4.4.3 객체 식
코틀린은 명시적인 선언 없이 객체를 바로 생성할 수 있는 특별한 식을 제공한다. 객체 식(object expression)은 자바 익명 클래스(anonymous class)와 아주 비슷하다.
fun main(){ fun midPoint(xRange: IntRange, yRange: IntRange) = object { val x = (xRange.first + xRange.last)/2 val y = (yRange.first + yRange.last)/2 } val midPoint = midPoint(1..5, 2..6) println("${midPoint.x}, ${midPoint.y}") //(3, 4) }
객체 식은 이름이 없는 객체 정의처럼 보인다. 그리고 객체 식도 식이므로 예제처럼 객체 식이 만들어내는 값을 변수에 대입할 수 있다. 클래스나 객체 식과 달리 객체를 함수 안에 정의할 수는 없다.
fun printMiddel(xRange: IntRange, yRange: IntRange) { // error : Named object 'MidPoint' is a singleton and cannot be local. Try to use anonymous object instead object MidPoint{ val x = (xRange.first + xRange.last)/2 val y = (yRange.first + yRange.last)/2 } }
이렇게 결정한 이유는 객체 선언이 싱글턴을 표현하지만 지역 객체들은 자신을 둘러싼 바깥 함수가 호출될 때마다 매번 다시 생성돼야 하기 때문이다.
midPoint()가 반환하는 타입은 익명 객체 타입이다. 객체 식 안에 정의된 모든 맴버가 들어있는 클래스를 표현하고, 이런 타입은 단 하나만 존재한다. (맴버가 완전히 똑같아도 타입은 다르다)
다음 예제는 객체 식의 타입이 익명 객체 타입이며, 지역 변수나 프로퍼티의 타입도 마찬가지일수 있다.
fun main(){ val o = object { // 익명 객체 타입으로 추론됨 val x = readLine()!!.toInt() val y = readLine()!!.toInt() } println(o.x + o.y) }
익명 객체 타입은 지역 선언이나 비공개 선언에만 전달될 수 있다. 예를 들어 최상위 함수로 정의하면 객체맴버에 접근할 때 컴파일 오류가 발생한다.
지연 초기화되는 객체 선언과 달리 객체 식이 만들어내는 객체는 객체 인스턴스가 생성된 직후 바로 초기화된다.
fun main(){ var x = 1 val o = object { val a = x++ } println("o.a = ${o.a}") // o.a = 1 println("x = $x") // x = 2 }
객체 식은 클래스 상속과 조합했을 때 더 강력해진다. 객체 식은 기존 클래스의 하위 클래스를 선언하지 않고도 기존 클래스를 약간만 변경해 기술하는 간결한 방법을 제공한다.
'코틀린 공부 > 코틀린' 카테고리의 다른 글
[코틀린 완벽 가이드] 5.1 코틀린을 활용한 함수형 프로그래밍 (0) 2024.01.03 [코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.3 단순한 변수 이상 인 프로퍼티) (0) 2023.12.30 [코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.2 널 가능성) (0) 2023.12.29 [코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.1 클래스 정의하기) (0) 2023.12.29 [코틀린 완벽 가이드] 3장 함수 정의하기 (3.5 예외 처리) (0) 2023.12.29