object 키워드를 사용하는 여러 상황
- 객체 선언(object declaration)은 싱글턴을 정의하는 방법 중 하나다.
- 동반 객체(companion object)는 인스턴스 메소드는 아니지만 어떤 클래스와 관련 있는 메소드와 팩토리 메소드를 담을 때 쓰인다. 동반 객체 메소드에 접근할 때는 동반객체가 포함된 클래스의 이름을 사용할 수 있다.
- 객체 식은 자바의 무명 내부 클래스(anonymous inner class)대신 쓰인다.
객체선언: 싱글턴을 쉽게 만들기
코틀린은 객체 선언 기능을 통해 싱글턴을 언어에서 기본 지원한다. 객체 선언은 클래스 선언과 그 클래스에 속한 단일 인스턴스 의 선언을 합친 선언이다.
한 회사에 여러 급여 대장이 필요하지 않을테니 회사 급여 대장은 싱글턴으로 쓰는게 정당하다.
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary(){
for (person in allEmployees) {
...
}
}
}
객체 선언은 object 키워드로 시작하며, 클래스와 마찬가지로 프로퍼티, 메소드, 초기화 블록 등이 들어갈 수 있다. 하지만 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어지기 때문에 생성자를 쓸 수 없다. 따라서 객체 선언에는 생성자 정의가 필요 없다.
Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()
클래스나 인터페이스를 상속할 수 있다. 두 파일 경로를 대소문자 관계없이 비교해주는 Comparator를 구현해보자.
object CaseInsensitiveFileComparator: Comparator<File> {
override fun compare(file1: File, file2: File): Int{
return file1.path.compareTo(file2.path, ignoreCase =true)
}
}
>>> println(CaseInsensitiveFileComparator(File("/User"), File("/user"))
0
// 일반 객체를 사용할 수 있는 곳에서는 항상 싱글턴 객체를 사용할 수 있다.
>>> val files = listOf(File("/Z"), File("/a"))
>>> println(files.sortedWith(CaseInsensitiveFileComparator))
[/a, /Z]
클래스 안에서 객체를 선언할 수도 있는데 그런 객체도 인스턴스는 단 하나뿐이다. (바깥 클래스의 인스턴스마다 중첩 객체 선언에 해당하는 인스턴스가 하나씩 따로 생기는 것이 아님)
ex. 어떤 클래스의 인스턴스를 비교하는 Comparator를 클래스 내부에 정의하는게 더 바람직하다.
data class Person(val name: String){
object NameComparator: Comparator<Person>{
override fun compare(p1: Person, p2: Person): Int = p1.name.compareTo(p2.name)
}
}
>>> var persons = listOf(Person("Corn"), Person("ACorn"))
>>> println(persons.sortedWith(Person.NameComparator))
[Person(name=ACorn), Person(name=Corn)]
자바에서 코틀린 객체를 사용하는 방법
// 자바 코드에서 코틀린 싱글턴 객체를 사용하려면 정적인 INSTANCE 필드를 통하면 된다.
CaseInsensitiveFileComparator.INSTANCE.compare(file1, file2);
동반 객체: 팩토리 메소드와 정적 멤버가 들어갈 장소
코틀린 언어에서는 static 키워드를 지원하지 않는다. 대신 패키지 수준의 최상위 함수(자바의 정적 메소드 역할을 거의 대신할 수 있음)와 객체 선언(자바의 정적 메소드 역할 중 코틀린 최상위 함수가 대신할 수 없는 역할이나 정적 필드를 대신할 수 있다)를 활용한다.
대부분의 경우 최상위 함수를 활용하는 것을 권장하지만 private 으로 표시된 클래스 비공개 멤버에 접근할 수 없다. 그래서 클래스의 인스턴스와 관계없이 호출해야 하지만, 클래스 내부 정보에 접근해야 하는 함수가 필요할 때는 클래스에 중첩된 객체 선언의 멤버 함수로 정의해야 한다. 그런 함수의 대표적인 예로 팩토리 메소드를 들 수 있다.
클래스 안에 정의된 객체 중 하나에 companion이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다. 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.
class A{
companion object{
fun bar(){
println("Companion object called")
}
}
}
>>> A.bar()
Companion object called
동반 객체가 private 생성자를 호출하기 좋은 위치다. 동반 객체는 자신을 둘러싼 클래스의 모든 pirvate 멤버에 접근할 수 있다. 따라서 동반 객체는 바깥쪽 클래스의 private 생성자도 호출할 수 있다. 동반 객체는 팩토리 패턴을 구현하기 가장 적합한 위치다.
// 부 생성자가 2개 있는 클래스
class User{
var nickname: String
// 부 생성자
constructor(email: String){
nickname = email.substringBefore('@')
}
constructor(facebookAccountId: Int){
nickname = getFacebookName(facebookAccountId)
}
}
더 유용한 방법으로 클래스의 인스턴스를 생성하는 팩토리 메소드가 있다.
// 동반 객체안에서 팩토리 클래스를 정의하는 방식
class User private constructor(val nickname: String){
// 주 생성자를 비공개로 만든다.
companion object{
// 동반 객체를 선언한다.
fun newSubscribingUser(email: String) =
User(email.substringBefore('@'))
fun newFacebookUser(accountId: Int) =
User(getFacebookName(accountId))
}
}
클래스 이름을 사용해 그 클래스에 속한 동반 객체의 메소드를 호출할 수 있다.
>>> val subscribingUser = User.newSubscribingUser("bob@gmail.com")
>>> val facebookUser = User.newFacebookUser(4)
>>> println(subscribingUser.nickname)
bob
동반 객체를 일반 객체처럼 사용
동반 객체는 클래스 안에 정의된 일반 객체다. 따라서 동반 객체에 이름을 붙이거나, 동반 객체가 인터페이스를 상속하거나, 동반 객체 안에 확장 함수와 프로퍼티를 정의할 수 있다.
// 동반 객체에 이름 붙이기
class Person(val name: String){
companion object Loader{
fun fromJSON(jsonText: String): Preseon = ...
}
}
// 두 방법 모두 호출 가능하다.
>>> person = Person.Loader.fromJSON("{name:'corn'}")
>>> person.name
corn
>>> person2 = Person.Loader.fromJSON("{name:'corn'}")
>>> person2.name
Corn
동반 객체에서 인터페이스 구현
동반 개체도 인터페이스를 구현할 수 있다. 인터페이스를 구현하는 동반 객체를 참조할 때 객체를 둘러싼 클래스의 이름을 바로 사용할 수 있다.
⇒ JSON을 역직렬화하는 JSONFactory 인터페이스 존재
// 동반 객체에서 인터페이스 구현하기
interface JSONFactory<T>{
fun fromJSON(jsonText: String): T
}
class Person(val name: String){
companion object: JSONFactory<Person>{
// 동반객체가 인터페이스 구현
override fun fromJSON(jsonText: String): Person = ...
}
}
JSON으로부터 각 원소를 다시 만들어내는 추상 팩토리가 있다면 Person 객체를 그 팩토리에게 넘길 수 있다.
fun loadFromJSON<T>(factory: JSONFactory<T>): T{
...
}
loadFromJSON(Person)
동반 객체가 구현한 JSONFacotry인스턴스를 넘길 때 Person 클래스의 이름을 사용했다.
동반 객체 확장
C 라는 클래스 안에 동반 객체가 있고 그 동반 객체(C.Companion)안에 func를 정의하면 외부에서는 func()를 C.func()로 호출할 수 있다.
Person의 관심사를 분리해보자. 역직렬화 함수를 비즈니스 모듈이 아니라 클라이언트/서버 통신을 담당하는 모듈 안에 포함시키고 싶다면 아래와 같이 코드를 변경한다.
// 동반 객체에 대한 확장 함수 정의하기
// 비즈니스 로직 모듈
class Pserson(val firstName: String, val lastName: String){
companion object{
// 비어있는 동반 객체 선언
}
}
// 클라이언트/서버 통신 모듈
fun Person.Companion.fromJSON(json: String): Person{
// 확장 함수 선언
...
}
val p = Person.fromJSON(json)
마치 동반 객체 안에서 fromJSON 함수를 정의한 것처럼 fromJSON을 호출할 수 있다. (실제로는 클래스 밖에서 정의한 확장 함수)
'프로그래밍 노트 > Kotlin' 카테고리의 다른 글
[Kotlin] 코틀린 컬렉션 함수 API(filter, map ...) (0) | 2020.12.06 |
---|---|
[Kotlin] 코틀린 람다 맛보기 (0) | 2020.12.06 |
[Kotlin] 코틀린 데이터 클래스와 클래스 위임 (0) | 2020.11.30 |
[Kotlin]인터페이스에 선언된 프로퍼티 구현/프로퍼티 게터 세터 (0) | 2020.11.30 |
[Kotlin] 코틀린 생성자(주/부생성자) (0) | 2020.11.30 |