Object 키워드
코틀린에서는 object 키워드를 다양한 상황에서 사용하지만, 모든 경우 클래스를 정의하면서 동시에 인스턴스(객체)를 생성한다는 공통점이 있다.
object 키워드를 사용하는 여러 상황을 살펴보자.
상황 | 설명 |
객체 선언 (object declaration) | 싱글턴을 정의하는 방법 중 하나다. |
동반 객체 (companion object) | 인스턴스 메서드는 아니지만 어떤 클래스와 관련있는 메서드와 팩토리 메서드를 담을때 쓰인다. 동반 객체 메서드에 접근할때는 동반 객체가 포함된 클래스의 이름을 사용할 수 있다. |
객체 식은 자바의 무명 내부 클래스 대신 쓰인다. |
객체 선언 (object declaration)
1) 객체 선언 : 싱글턴을 쉽게 만들기
코틀린은 객체 선언 기능을 통해 싱글턴(인스턴스가 하나만 필요한 클래스)을 기본 지원한다. 객체 선언은 클래스 선언과 그 클래스에 속한 단일 인스턴스의 선언을 합친 선언이다.
object Payroll { // object 키워드로 시작
val allEmployees = arrayListOf<Person>()
fun calculateSalary() {
for (person in allEmployees) {
}
}
}
객체 선언은 클래스를 선언하고 그 클래스의 인스턴스를 만들어서 변수에 저장하는 모든 작업을 한문장으로 처리한다. 생성자를 객체 선언에 쓸 수 없다. 인스턴스와 달리 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어진다.
호출
fun main() {
Payroll.allEmployees.add(Person())
Payroll.calculateSalary()
}
객체 선언에 사용한 이름 뒤에 마침표(.)를 붙이면 객체에 속한 메서드나 프로퍼티에 접근할 수 있다.
2) 객체 선언도 클래스나 인터페이스를 상속할 수 있다.
프레임워크를 사용하기 위해 특정 인터페이스를 구현해야하는데, 그 구현 내부에 다른 상태가 필요하지 않은 경우에 이런 기능이 유용하다.
object CaseInsensitiveFileComparator : Comparator<File> {
override fun compare(file1: File, file2: File): Int {
return file1.path.compareTo(file2.path, ignoreCase = true)
}
}
java.util.Comparator 구현은 두 객체를 인자로 받아 그 중 어느 객체가 더 큰지 알려주는 정수를 반환한다.
Comparator 안에는 데이터를 저장할 필요가 없다.
따라서 어떤 클래스에 속한 객체를 비교할때 사용하는 Comparator는 보통 클래스마다 단 하나씩만 있으면 된다. 따라서 Comparator 인스턴스를 만드는 방법으로는 객체 선언이 가장 좋은 방법이다.
3) 클래스 안에서 객체를 선언할 수도 있다.
그런 객체도 인스턴스는 단 하나뿐이다.
data class Person(val name: String) {
object NameComparator : Comparator<Person> {
override fun compare(p1: Person, p2: Person): Int =
p1.name.compareTo(p2.name)
}
}
호출
fun main() {
val persons = listOf(Person("bob"), Person("seohae"))
println(persons.sortedWith(Person.NameComparator))
}
동반 객체 (companion object)
코틀린 클래스 안에는 정적인 멤버가 없다. 코틀린 언어는 자바 static 키워드를 지원하지 않는다. 그 대신 코틀린에서는 패키지 수준의 최상위 함수와 객체 선언을 활용한다.
1) 패키지 수준의 최상위 함수 : 자바의 정적 메서드 역할을 거의 대신할 수 있다.
2) 객체 선언 : 자바의 정적 메서드 역할 중 코틀린 최상위 함수가 대신할 수 없는 역할이나 정적 필드를 대신할 수 있다.
대부분의 경우 최상위 함수 활용을 더 권장한다. 하지만 최상위 함수는 private로 표시된 클래스 비공개 멤버에 접근할 수 없다. 그래서 클래스의 인스턴스와 관계없이 호출해야 하지만, 클래스 내부 정보에 접근해야 하는 함수가 필요할 때는 클래스의 중첩된 객체 선언의 멤버 함수로 정의해야한다.
클래스 안에 정의된 객체 중 하나에 companion 이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다. 동반 객체의 프로퍼티나 메서드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.
그 결과 동반 객체의 멤버를 사용하는 구문은 자바의 정적 메서드 호출이나 정적 필드 사용 구문과 같아진다.
1) 동반 객체는 자신을 둘러싼 모든 private 멤버에 접근할 수 있다.
따라서 동반 객체는 바깥쪽 클래스의 private 생성자를 호출할 수 있다.
class A {
companion object {
fun bar() {
println("Companion object called")
}
}
}
호출
fun main() {
A.bar() // Companion object called
}
2) 팩토리 메서드로 구현할 수 있다.
Before
class CompanionUser {
val nickname: String
constructor(email: String) { // 부 생성자
nickname = email.substringBefore('@')
}
constructor(facebookAccountId: Int) { // 부 생성자
nickname = getFacebookName(facebookAccountId)
}
}
After
// 팩토리 메서드
class CompanionFactoryUser private constructor(val nickname: String) { // 주 생성자를 비공개로 만든다.
companion object { // 동반 객체를 선언한다.
fun newSubscribingUser(email: String) = CompanionFactoryUser(email.substringBefore('@'))
fun newFacebookUser(accountId: Int) = CompanionFactoryUser(getFacebookName(accountId))
}
}
팩토리 메서드는 매우 유용하다. 이름을 정할 수 있고, 그 팩토리 메서드가 선언된 클래스의 하위 클래스 객체를 반환할 수도 있다. 하지만 클래스를 확장해야만 하는 경우에는 동반 객체 멤버를 하위 클래스에서 오버라이드할 수 없으므로 여러 생성자를 사용하는 것이 낫다.
호출
fun main() {
val result = CompanionFactoryUser.newSubscribingUser("sss@test.com")
print(result.nickname)
}
3) 동반 객체를 일반 객체처럼 사용할 수 있다.
동반 객체는 클래스 안에 정의된 일반 객체다. 따라서 동반 객체에 이름을 붙이거나, 동반 객체가 인터페이스를 상속하거나, 동반 객체 안에 확장 함수와 프로퍼티를 정의할 수 있다.
class Person2(val name: String) {
companion object Loader {
fun fromJSON(jsonText: String): Person2 = Person2(jsonText) // 동반 객체가 인터페이스 구현
}
}
호출
fun main() {
// 클래스 이름을 통해 동반 객체에 속한 멤버를 참조할 수 있다.
// 필요하다면 Person2.Loader 같은 방식으로 이름을 붙일 수도 있다.
// 이름을 지정하지 않으면 동반 객체 이름은 자동으로 Companion이 된다.
val seohae = Person2.Loader.fromJSON("seohae")
println(seohae.name)
val seohae2 = Person2.fromJSON("seohae2")
println(seohae.name)
}
4) 동반 객체도 인터페이스를 구현할 수 있다.
interface JSONFactory<T> {
fun fromJSON(jsonText: String): T
}
class Person3(val name: String) {
companion object : JSONFactory<Person3> {
override fun fromJSON(jsonText: String): Person3 = Person3(jsonText) // 동반 객체가 인터페이스 구현
}
}
이제 JSON으로부터 각 원소를 다시 만들어내는 추상 팩토리가 있다면 Person3 객체를 그 팩토리에게 넘길 수 있다.
fun loadFromJSON<T>(factory: JSONFactory<T>): T {
...
}
loadFromJSON(Person3) // 동반 객체의 인스턴스를 함수에 넘긴다. (Person3 클래스의 이름을 사용했다.)
5) 동반 객체를 확장할 수 있다.
확장 함수를 사용하면 코드 기반의 다른 곳에서 정의된 클래스의 인스턴스에 대해 새로운 메서드를 정의할 수 있었다. 클래스에 동반 객체가 있으면 그 객체 안에 함수를 정의함으로써 클래스에 대해 호출할 수 있는 확장 함수를 만들 수 있다.
C 클래스 안에 동반 객체가 있고, 그 동반 객체(C.Companion) 안에 func를 정의하면 외부에서는 func()를 C.func()으로 호출할 수 있다.
동반 객체 생성
class Person4(val firstName: String, val lastName: String) {
companion object {
// 비어 있는 동반 객체를 선언한다.
}
}
확장 함수 선언
fun Person4.Companion.fromJSON(json: String): Person { // 확장 함수를 선언한다.
//...
}
사용
마치 동반 객체 안에서 fromJSON 함수를 정의한 것처럼 호출할 수 있다.
실제로 fromJSON 는 클래스 밖에서 정의된 확장함수다. (멤버 함수가 아니다.) 동반 객체에 대한 확장 함수를 작성할 수 있으려면, 원래 클래스에 동반 객체를 꼭 선언해줘야한다.
val p = Person4.fromJSON("aaa")
무명객체
object 키워드를 싱글턴과 같은 객체를 정의하고 그 객체에 이름을 붙일 때만 사용하지 않는다. 무명 객체(anonymous object) 를 정의할 때도 object 키워드를 쓴다. 무명 객체는 자바의 무명 내부 클래스를 대신한다.
자바의 무명 내부 클래스 예제
public class Test {
public static void main(String[] args) {
Target target = new Target();
target.execPrint(new AbstractTest() {
public void createMsg() {
System.out.println("내부 클래스 함수 실행!");
}
});
});
}
1) 객체 이름이 없다.
객체 식은 클래스를 정의하고 그 클래스에 속한 인스턴스를 생성하지만, 그 클래스나 인스턴스에 이름을 붙이지는 않는다.
이런 경우 보통 함수를 호출하면서 인자로 무명 객체를 넘기기 때문에 클래스와 인스턴스 모두 이름이 필요하지 않다.
window.addMouseListener(
object : MouseAdapter() { // MouseAdapter를 확장하는 무명 객체를 선언한다.
override fun mouseClicked(e: MouseEvent) { // MouseAdapter의 메서드를 오버라인드한다.
// ...
}
override fun mouseEntered(e: MouseEvent) {
// ...
}
}
)
2) 객체에 이름을 붙여야 한다면 변수에 무명 객체를 대입하면 된다.
val listener = object : MouseAdapter() { // MouseAdapter를 확장하는 무명 객체를 선언한다.
override fun mouseClicked(e: MouseEvent) { // MouseAdapter의 메서드를 오버라인드한다.
// ...
}
override fun mouseEntered(e: MouseEvent) {
// ...
}
}
3) 로컬 변수에 접근할 수 있다.
자바 : 한 인터페이스만 구현하거나 한 클래스만 확장할 수 있는 자바의 무명 내부 클래스
코틀린 : 여러 인터페이스를 구현하거나 클래스를 확장하면서 인터페이스를 구현할 수 있다.
자바의 무명 클래스와 같이 객체 식 안의 코드는 그 식이 포함된 함수의 변수에 접근할 수 있다.
하지만 자바와 달리 final이 아닌 변수도 객체 식 안에서 사용할 수 있다. 객체 식 안에서 그 변수의 값을 변경할 수 있다.
fun countClicks(window: Window) {
var clickCount = 0 // 로컬 변수 정의
window.addMouseListener(object : MouseAdapter) {
override fun mouseClicked(e: MouseEvent) {
clickCount++ // 로컬 변수의 값 변경
}
}
}
'Coding > Kotlin' 카테고리의 다른 글
[Kotlin in Action] 22. 람다식의 로컬 변수 접근, 멤버 참조 (0) | 2022.05.30 |
---|---|
[Kotlin in Action] 21. 람다식 (0) | 2022.05.29 |
[Kotlin in Action] 19. 코틀린의 toString(), equals(), hashCode(), copy() 메서드 구현과 자동생성 data 변경자, 클래스 위임 by (2) | 2022.05.26 |
[Kotlin in Action] 18. 인터페이스의 프로퍼티 구현, 뒷받침하는 필드(field 식별자), 접근자의 가시성 변경 (0) | 2022.05.25 |
[Kotlin in Action] 17. 클래스의 생성자와 초기화 블록, 주 생성자와 부 생성자 (0) | 2022.05.24 |