코틀린에서 클래스를 정의하는 키워드는 class
입니다.
간혹 object
키워드로 클래스 정의하는 경우를 볼 수 있습니다.
object
로 클래스를 정의하면, 싱클턴(Singleton) 패턴이 적용되어 객체가 한번만 생성되도록 합니다.
자바에서는 싱글턴 패턴을 적용하기 위해 꽤 많은 코드를 작성해야 했는데요, 코틀린에서는 object
를 사용하면 이런 형식적인 코드(boilerplate)를 작성하지 않아도 됩니다.
싱글턴으로 사용하는 방법 외에도, object는 익명객체를 생성할 때도 사용됩니다.
정리하면, object 다음과 같은 경우에 사용되며 예제코드와 함께 자세히 살펴보겠습니다.
- 싱글턴 클래스로 만들 때
- 익명 클래스 객체를 생성할 때
싱글턴 클래스를 정의를 위한 object 예제
object로 싱글턴 클래스를 정의할 수 있습니다.
아래 코드에서 CarFactory 클래스를 정의할 때 class
가 있어야 할 위치에 object
를 입력해주면 이 클래스는 싱글턴으로 동작하게 됩니다.
object CarFactory {
val cars = mutableListOf<Car>()
fun makeCar(horsepowers: Int): Car {
val car = Car(horsepowers)
cars.add(car)
return car
}
}
class Car(power: Int) {
}
아래 코드처럼 CarFactory.makeCar
처럼 메소드에 접근하여 Car객체를 생성할 수 있습니다. 또한, CarFactory.cars
처럼 직접 변수에 접근할 수 있습니다.
CarFactory 객체는 싱글턴으로 구현이 되었기 때문에 여러번 호출해도 CarFactory 객체는 한번만 생성이 됩니다.
val car = CarFactory.makeCar(150)
println(CarFactory.cars.size)
위 코드를 보면, CarFactory.makeCar()는 static 메소드를 호출하는 것처럼 보입니다. 자바로 어떻게 변환이 되는지 보면 싱글턴이 내부적으로 어떻게 구현되는지 이해할 수 있습니다.
public final class CarFactory {
private static final List cars;
public static final CarFactory INSTANCE;
public final List getCars() {
return cars;
}
public final Car makeCar(int horsepowers) {
Car car = new Car(horsepowers);
cars.add(car);
return car;
}
static {
CarFactory var0 = new CarFactory();
INSTANCE = var0;
cars = (List)(new ArrayList());
}
}
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Car car = CarFactory.INSTANCE.makeCar(150);
int var2 = CarFactory.INSTANCE.getCars().size();
System.out.println(var2);
}
위의 자바로 변환된 코드를 보면 CarFactory 객체는 INSTANCE라는 static 객체를 생성합니다.
그리고 이 객체에 접근할 때 CarFactory.INSTANCE
를 통해서 접근하게 됩니다. INSTANCE는 static으로 생성되기 때문에 프로그램이 로딩될 때 생성됩니다. 그래서 쓰레드 안전성(thread-safety)이 보장됩니다만, 내부적으로 공유자원을 사용하는 경우 쓰레드 안전성이 보장되지 않기 때문에 동기화(synchronization) 코드를 작성해야 합니다.
싱글턴 클래스 정의를 위한 companion object 예제
위의 예제는 CarFactory객체가 Car객체를 생성해주는 구현이었습니다. 여기서 팩토리 패턴과, 싱글턴 패턴이 함께 적용되었는데요. Car 클래스 안에 Factory 패턴을 정의하고 싶을 수 있습니다.
Car.makeCar
처럼 호출을하는 것이 직관적으로 좋아보여서요. 이럴 때는 companion object로 선언해주면 됩니다.
이런식으로 Car 안에 companion object로 Factory를 정의해주면 Car.makeCar()
처럼 호출할 수 있습니다.
사실 Car.Factory.makeCar()
로 호출해주는 것이 명시적으로 정확한 표현입니다만, 코틀린은 편의를 위해 Factory를 생략할 수 있게 해주었습니다.
class Car(val horsepowers: Int) {
companion object Factory {
val cars = mutableListOf<Car>()
fun makeCar(horsepowers: Int): Car {
val car = Car(horsepowers)
cars.add(car)
return car
}
}
}
fun main(args: Array<String>) {
val car = Car.makeCar(150)
val car2 = Car.Factory.makeCar(150)
println(Car.Factory.cars.size)
}
변환된 자바 코드를 보시면 Car 클래스 안에 중첩클래스(nested class)로 Factory 클래스가 정의되어있습니다.
또한 Car.Factory
클래스는 Factory라는 이름의 static 객체로 선언하였습니다.
외부에서 Car.Factory.makeCar
처럼 사용할 수 있는데 여기서 Factory는 클래스 이름이 아니라 static 변수의 이름이었습니다.
코틀린에서 makeCar를 두가지 방식으로 호출했는데요, 자바에서는 동일한 코드로 호출한다는 것을 알 수 있습니다.
public final class Car {
private final int horsepowers;
private static final List cars = (List)(new ArrayList());
public static final Car.Factory Factory = new Car.Factory((DefaultConstructorMarker)null);
public final int getHorsepowers() {
return this.horsepowers;
}
public Car(int horsepowers) {
this.horsepowers = horsepowers;
}
public static final class Factory {
@NotNull
public final List getCars() {
return Car.cars;
}
@NotNull
public final Car makeCar(int horsepowers) {
Car car = new Car(horsepowers);
((Car.Factory)this).getCars().add(car);
return car;
}
private Factory() {
}
// $FF: synthetic method
public Factory(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Car car = Car.Factory.makeCar(150);
Car car2 = Car.Factory.makeCar(150);
int var3 = Car.Factory.getCars().size();
System.out.println(var3);
}
object를 익명객체로 사용한 예제
object는 익명객체를 정의할 때도 사용됩니다. 익명객체는 이름이 없는 객체로, 한번만 사용되고 재사용되지 않을 때 사용합니다. 재사용되지 않기 때문에 귀찮게 클래스 이름을 지어주지 않는 것이죠.
예를들어, 아래와 같이 Vehicle 인터페이스, start() 메소드가 정의되어있습니다. start()는 Vehicle 객체를 인자로 전달받습니다.
interface Vehicle {
fun drive(): String
}
fun start(vehicle: Vehicle) = println(vehicle.drive())
아래 코드에서 start()의 인자로 전달되는 object : Vehicle{...}
는 익명객체입니다.
이 익명객체는 Vehicle 인터페이스를 상속받은 클래스를 객체로 생성된 것을 의미합니다. 익명객체이기 때문에 클래스 이름은 없고, 구현부는 {...}
안에 정의해야 합니다.
start(object : Vehicle {
override fun drive() = "Driving really fast"
})
위 코드도 자바로 변환해서 살펴보겠습니다. 자바도 코틀린처럼 Vehicle를 상속한 익명객체를 만들었습니다.
(참고로, 코틀린에서 fun start()
는 파일의 Top-level에 정의되었기 때문에 자바에서 static 메소드로 생성이 되었습니다.)
public interface Vehicle {
@NotNull
String drive();
}
public final class KotlinKt {
public static final void start(@NotNull Vehicle vehicle) {
String var1 = vehicle.drive();
System.out.println(var1);
}
public static final void main(@NotNull String[] args) {
start((Vehicle)(new Vehicle() {
@NotNull
public String drive() {
return "Driving really fast";
}
}));
}
}
정리
object의 사용방법에 대해서 알아보았습니다. 클래스를 정의할 때 object를 사용하면 싱글턴 패턴이 적용되고, object를 사용하여 익명객체를 생성할 수도 있었습니다. 만약 자바개발자라면, object 코드가 어떻게 자바로 변환되는지 살펴보는 것이 좋습니다. 코틀린 코드에 대한 이해도를 높일 수 있습니다.
참고
Related Posts
- Kotlin - 배열에서 최소 값, 최대 값 찾기
- Kotlin - 2차원 배열 선언, 초기화 방법
- Kotlin - 배열 선언, 초기화 방법
- Kotlin - 리스트, 배열 길이 가져오기
- Kotlin - 리스트에서 최대, 최소 값 찾기
- Kotlin - for 반복문, 배열/리스트 순회
- Kotlin - Timer, 주기적으로 함수 실행
- Kotlin - sleep, 쓰레드 몇 초 지연
- Kotlin - Thread 생성 및 실행
- Kotlin에서 정규표현식 사용하기
- Kotlin - 문자열 길이 계산
- Kotlin - 문자열 비교 방법(equals, ==, compareTo)
- Kotlin - 2개의 배열 하나로 합치기
- Kotlin - 2개의 List 하나로 합치기
- Kotlin - 디렉토리의 모든 파일 리스트 출력
- Kotlin - 리스트 정렬 방법 (sort, sortBy, sortWith)
- Kotlin - 문자열 뒤집기 (Reverse String)
- Kotlin - 랜덤 숫자 생성 (Random, SecureRandom)
- Kotlin - Range, 숫자 범위 표현
- Kotlin - 음수를 양수로 변환, math.abs()
- Kotlin - List를 Set로 변환
- Kotlin - Set를 List로 변환
- Kotlin - 문자열에서 숫자(int)만 추출하는 방법
- Kotlin - Map을 List로 변환하는 방법
- Kotlin - File, Directory가 존재하는지 확인
- Kotlin - List를 Map으로 변환
- Kotlin - List의 중복 요소 제거
- Kotlin - List를 Array로 변환
- Kotlin - 엘비스 연산자 (Elvis Operation)
- Kotlin - Array를 List로 변환
- Kotlin - String을 Float으로 변환
- Kotlin - String을 Double으로 변환
- Kotlin - String을 Int로 변환
- Kotlin - String을 Long으로 변환
- Kotlin - String Null 또는 Empty 체크