HOME > kotlin > basic

Kotlin - object와 class 키워드의 차이점

JSFollow01 Jun 2019

코틀린에서 클래스를 정의하는 키워드는 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 코드가 어떻게 자바로 변환되는지 살펴보는 것이 좋습니다. 코틀린 코드에 대한 이해도를 높일 수 있습니다.

참고