코틀린에서 object 키워드를 다양한 상황에서 사용하지만 모든 경우 클래스를 정의하면서 동시에 인스턴스(객체)를 생성한다는 공통점이 있습니다. object 키워드를 사용하는 상황은 아래와 같습니다.
객체지향에서 인스턴스가 하나만 필요한 클래스가 유용한 경우가 있는데 자바에서는 보통 클래스의 생성자를 private으로 제한하고 정적인 필드에 클래스의 유일한 객체를 저장하는 싱글턴 패턴을 통해 이를 구현합니다.
public class Payroll {
// 정적인 필드에 객체 저장
private static Payroll instance = new Payroll();
// 정적 메서드를 통해 객체 반환
public static Payroll getInstance() {
return instance;
}
// 생성자 private으로 설정
private Payroll() {
}
}
코틀린은 객체 선언 기능을 통해 싱글턴을 언어에서 기본 지원합니다. 객체 선언은 클래스 선언과 그 클래스에 속한 단일 인스턴스의 선언을 합친 선언입니다. 아래 예시는 코틀린에서 객체 선언 예제입니다.
// 객체 선언도 클래스나 인터페이스 상속 가능
// object 키워드를 사용해 클래스 선언과 클래스에 속한 단일 인서턴스의 선언(객체 선언)
// 싱글턴에 해당함
object CaseInsensitiveFileComparator: Comparator<File> {
override fun compare(file1: File, file2: File): Int {
return file1.path.compareTo(file2.path, ignoreCase = true)
}
}
fun main() {
val file1: File = File("/User")
val file2: File = File("/User")
// 마침표를 사용하여 객체 선언의 메서드에 접근
println(CaseInsensitiveFileComparator.compare(file1, file2))
val files = listOf(file1, file2)
// 일반 객체를 사용할 수 있는 곳에서 싱글턴 객체 사용 가능
// Comparator를 인자로 받는 함수에 객체를 전달
println(files.sortedWith(CaseInsensitiveFileComparator))
}
결과: 0(같음)
객체 선언은 object 키워드로 시작합니다
객체 선언은 클래스를 정의하고 그 클래스의 인스턴스(객체)를 만들어서 변수에 저장하는 모든 작업을 단 한 문장으로 처리합니다
클래스와 마찬가지로 객체 선언 안에도 프로퍼티, 메서드, 초기화 블록 등이 들어갈 수 있습니다.
일반 클래스의 인스턴스와 달리 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어지기에 생성자는(주 생성자와 부 생성자 모두) 객체 선언에 쓸 수 없습니다
변수와 마찬가지로 객체 선언에 사용한 이름 뒤에 마침표(.)를 붙이면 객체에 속한 메서드와 프로퍼티에 접근할 수 있습니다
객체 선언도 클래스나 인터페이스를 상속할 수 있습니다
일반 객체(클래스 인스턴스)를 사용할 수 있는 곳에서는 항상 싱글턴 객체를 사용할 수 있습니다(코드에서 sortedWith의 매개변수로 Comparator를 구현하는 싱글턴 객체를 전달)
싱글턴과 의존관계 주입
싱글턴 패턴과 마찬가지 이유로 대규모 소프트웨어 시스템에는 객체 선언이 항상 적합하지는 않습니다. 의존관계가 별로 많지 않은 소규모 소프트웨어에서는 싱글턴이나 객체 선언이 유용하지만 시스템을 구현하는 다양한 구성 요소와 상호작용하는 대규모 컴포넌트에는 싱글턴이 적합하지 않습니다. 그 이유는 객체 생성을 제어할 방법이 없고 생성자 파라미터를 지정할 수 없어서입니다.
클래스 안에도 객체를 선언할 수 있습니다. 이러한 객체도 인스턴스는 단 하나입니다(바깥 클래스의 인스턴스마다 중첩 객체 선언에 해당하는 인스턴스가 하나씩 따로 생기는 것이 아님). 예를 들어 어떤 클래스의 인스턴스를 비교하는 Comparator를 클래스 내부에 정의하면 바람직합니다.
// data 클래스
data class Person(val name: String) {
// Person 클래스의 인스턴스를 비교하는 Comparator
// 내부에 객체 선언
object NameComparator: Comparator<Person> {
override fun compare(person1: Person, person2: Person): Int =
person1.name.compareTo(person2.name)
}
}
fun main() {
val persons = listOf<Person>(Person("Bob"), Person("Alice"))
// 중첩 객체를 사용하여 sortedWith의 매개변수로 Comparator를 구현하는 객체 전달
println(persons.sortedWith(Person.NameComparator))
}
코틀린 클래스 안에는 정적인 멤버가 없습니다. 코틀린 언어는 자바 static 키워드를 지원하지 않습니다. 그 대신 코틀린에서는 패키지 수준의 최상위 함수(자바의 정적 메서드 역할을 거의 대신 함)와 객체 선언(자바의 정적 메서드 역할 중 코틀린 최상위 함수가 대신할 수 없는 역할이나 정적 필드를 대신 함)을 활용하면 됩니다.
대부분의 경우 최상위 함수를 활용하는 편을 더 권장하지만 최상위 함수는 private으로 표시된 클래스의 비공개 멤버에 접근할 수 없습니다. 그래서 클래스의 인스턴스와 관계없이 호출해야 하지만 클래스 내부 정보에 접근해야 하는 함수가 필요할 때는 클래스에 중첩된 객체 선언의 멤버 함수로 정의해야 합니다.
클래스 안에 정의된 객체 중 하나에 companion이라는 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있습니다. 동반 객체의 프로퍼티나 메서드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용합니다. 이때 객체의 이름을 따로 지정할 필요가 없어서 동반 객체의 멤버를 사용하는 구문은 자바의 정적 메서드 호출이나 정적 필드 사용 구문과 같아집니다.
class A {
// 동반 객체 선언
companion object {
fun bar() {
println(" Companion object")
}
}
}
fun main() {
// 동반 객체의 이름 지정없이 동반 객체의 메서드 호출
// 자바의 정적 메서드 호출과 같다
A.bar()
}
동반 객체는 클래스 안에 정의된 일반 객체입니다. 따라서 동반 객체에 이름을 붙이거나, 동반 객체가 인터페이스를 상속하거나, 동반 객체 안에 확장 함수와 프러퍼티를 정의할 수 있습니다.
class Person(val name: String) {
// 이름을 붙인 동반 객체
companion object Loader {
fun fromJson(jsonText: String): Person {
// 구현 코드
}
}
}
fun main() {
// 동반 객체의 이름을 사용하여 메서드 호출
val person = Person.Loader.fromJson("{name: '홍길동'}")
person.name
// 자바의 정적 메서드를 호출하 듯이 이름을 사용하지 않고 호출
val person2 = Person.fromJson("{name: '홍길동'}")
person2.name
}
필요하다면 위와 같이 동반 객체에도 이름을 붙일 수 있는데 특별히 이름을 지정하지 않으면 동반 객체 이름은 자동으로 Companion이 됩니다.
위에서 언급한듯이 동반 객체도 인터페이스를 구현할 수 있습니다. 그리고 인터페이스를 구현하는 동반객체를 참조할 때 객체를 둘러싼 클래스의 이름을 바로 사용할 수 있습니다.
// 인터페이스 선언
interface JSONFactory<T> {
fun fromJSON(jsonText: String): T
}
class Person(val name: String) {
// 동반 객체가 인터페이스를 구현
companion object: JSONFactory<Person> {
override fun fromJSON(jsonText: String): Person {
// 구현 코드
}
}
}
// JSONFactory 인터페이스를 매개변수로 넘기는 메서드
fun <T> loadFromJSON(factory: JSONFactory<T>): T {
}
fun main() {
// 동반 객체가 구현한 JSONFactory의 인스턴스를 넘길 때
// 동반 객체를 둘러싼 클래스를 넘김
loadFromJSON(Person)
}
위의 코드는 동반 객체가 인터페이스를 구현하는 코드입니다. 여기서 유의할 점은 동반 객체가 구현한 JSONFactory의 인스턴스를 넘길 때 Person 클래스의 이름을 사용했다는 점에 유의해야합니다.
클래스의 동반 객체는 일반 객체와 비슷한 방식으로, 클래스에 정의된 인스턴스를 가리키는 정적 필드로 컴파일됩니다. 동반 객체에 이름을 붙였다면 자바쪽에서 사용할 때 그 이름이 쓰이고 붙이지 않았다면 Companion이라는 이름으로 그 참조에 접근할 수 있습니다.
때로 자바에서 사용하기 위해 코틀린 클래스의 멤버를 정적인 멤버로 만들어야 할 필요가 있는데 그럴 경우 @JvmStatic 애노테이션을 코틀린의 멤버에 붙이면 됩니다. 정적 필드가 필요하다면 @JvmField 애노테이션을 최상위 프로퍼티나 객체에서 선언된 프로퍼티 앞에 붙입니다.
자바의 정적 메서드나 코틀린의 동반 객체 메서드처럼 기존 클래스에 대해 호출할 수 있는 새로운 함수를 정의하고 싶을 때, 클래스에 동반 객체가 있다면 그 객체 안에 함수를 정의함으로써 클래스에 대해 호출할 수 있는 확장 함수를 만들 수 있습니다.
// 비지니스 로직 모듈
class Person(val firstName: String, val lastName: String) {
// 비어있는 동반 객체를 선언
companion object {
}
}
// 클라이언트 / 서버 통신 모듈
// 확장 함수를 선언
fun Person.Companion.fromJson(json: String): Person {
// 구현 코드
}
val person = Person.fromJson("{name: '홍길동'}")
다른 보통 확장 함수처럼 fromJSON도 클래스 멤버 함수처럼 보이지만, 실제로는 멤버 함수가 아닙니다. 여기서 동반 객체에 대한 확장 함수를 작성할 수 있으려면 원래 클래스에 동반 객체를 꼭 선언해야 한다는 점에 주의해야 합니다. 설령 빈 객체라도 동반 객체가 꼭 있어야 합니다.
object 키워드를 싱글턴과 같은 객체를 정의하고 그 객체에 이름을 붙일 때만 사용하지는 않습니다. 무명 객체(anonymous object)를 정의할 때도 object 키워드를 사용합니다. 무명 객체는 자바의 무명 내부 클래스를 대신합니다. 자바에서 흔히 무명 내부 클래스로 구현하는 이벤트 리스너를 코틀린으로 구현하면 아래와 같습니다.
window.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
super.mouseClicked(e);
}
@Override
public void mouseEntered(MouseEvent e) {
super.mouseEntered(e);
}
});
window.addMouseListener(
object : MouseAdapter() { // MouseAdapter를 확장하는 무명 객체 선언
// MouseAdapter의 메서드 오버라이드
override fun mouseClicked(e: MouseEvent?) {
super.mouseClicked(e)
}
// MouseAdapter의 메서드 오버라이드
override fun mouseEntered(e: MouseEvent?) {
super.mouseEntered(e)
}
}
)
객체 선언과 비슷한데 한 가지 차이는 객체 이름이 빠졌다는 점입니다. 객체 식은 클래스를 정의하고 그 클래스에 속한 인스턴스를 생성하지만, 그 클래스나 인스턴스에 이름을 붙이지는 않습니다. 이런 경우 보통 함수를 호출하면서 인자로 무명 객체를 넘기기 때문에 클래스와 인스턴스 모두 이름이 필요하지 않습니다. 하지만 객체에 이름이 필요하다면 변수에 무명 객체를 대입하면 됩니다.
// 변수에 무명 객체 대입
val listener = object : MouseAdapter() {
// MouseAdapter의 메서드 오버라이드
override fun mouseClicked(e: MouseEvent?) {
super.mouseClicked(e)
}
// MouseAdapter의 메서드 오버라이드
override fun mouseEntered(e: MouseEvent?) {
super.mouseEntered(e)
}
}
한 인터페이스만 구현하거나 한 클래스만 확장하는 자바의 무명 내부 클래스와 달리 코틀린 무명 클래스는 여러 인터페이스를 구현하거나 클래스를 확장하면서 인터페이스를 구현할 수 있습니다.
자바의 무명 클래스와 같이 객체 식 안의 코드는 그 식이 포함된 함수의 변수에 접근할 수 있습니다. 하지만 자바와 다르게 final이 아닌 변수도 객체 식 안에서 사용할 수 있습니다. 따라서 객체 식 안에서 그 변수의 값을 변경할 수 있습니다.
fun countClicks(window: Window) {
// 로컬 변수 정의
var clickCount = 0
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
// final이 아닌 로컬 변수 접근 후 값 변경
clickCount++
}
})
}
객체 선언과 달리 무명 객체는 싱글턴이 아닙니다. 객체 식이 쓰일 때마다 새로운 인스턴스가 생성됩니다.
객체 식은 무명 객체 안에서 여러 메서드를 오버라이드해야 하는 경우에 훨씬 더 유용합니다. 메서드가 하나뿐인 인터페이스를 구현한다면 코틀린의 SAM 변환(Single Abstract Method - 추상 메서드가 하나만 있는 인터페이스) 지원을 활용하는 편이 더 좋습니다.
참조
Kotlin in Action
틀린 부분을 댓글로 남겨주시면 수정하겠습니다!!