[Kotlin in Action] 20. object 키워드 (객체 선언, 동반 객체, 무명 객체)

반응형
728x90
반응형

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++ // 로컬 변수의 값 변경
        }
    }
}

 

 

반응형

Designed by JB FACTORY