인프런/자바 개발자를 위한 코틀린 입

7) 코틀린에서 object 키워드, 중첩 클래스, 다양한 클래스를 다루는 방법

backend dev 2025. 1. 9.

1. static 함수와 변수

 

자바 코드)

public class JavaPerson {

  private static final int MIN_AGE = 1;

  public static JavaPerson newBaby(String name) {
    return new JavaPerson(name, MIN_AGE);
  }

  private String name;

  private int age;

  private JavaPerson(String name, int age) {
    this.name = name;
    this.age = age;
  }

}

 

코틀린으로 변환시

class Person private constructor(
    val name:String,
    val age:Int
){
    /*
    코틀린은 static이라는 키워드가 없다.
    static 대신 companion object을 이용한다.
     */
    companion object {
        private const val MIN_AGE: Int = 1

        fun newBaby(name: String): Person {
            return Person(name,MIN_AGE)

        }
    }
}

 

static : 클래스가 인스턴스화 될 때 새로운 값이 복제되는게 아니라 정적으로 인스턴스끼리의 값을 공유

 

private const val MIN_AGE: Int = 1

 

  • val은 런타임에 값을 초기화하며, 복잡한 계산이나 함수 호출 결과를 저장할 수 있습니다.
    • 초기화 시점은 런타임에 결정됩니다.
    • 컴파일 타임 상수는 아니므로, 런타임에 계산될 수 있는 값을 가질 수 있습니다.
  • const val은 컴파일 타임에 값을 결정하며, 프로그램 실행 시점에 변하지 않는 상수 값을 정의하는 데 사용됩니다.
    • 컴파일 타임에 상수로 결정됩니다.
    • 반드시 primitive 타입(Int, Long, Float, Double, Boolean, Char, String 등) 또는 String이어야 합니다.
    • 클래스 내부에서는 companion object 안에서만 사용할 수 있습니다.
    • 함수 내에서는 사용할 수 없습니다.

 

 

 

companion object : 클래스와 동행하는 유일한 오브젝트  [ 동반 객체 ] 

[ 즉 static처럼 여러 인스턴스가 있더라도 값을 공유한다. ] 

동반객체도 하나의 객체로 간주된다. 때문에 이름을 붙일 수도 있고, interface를 구현할 수도 있다.

class Person private constructor(
    val name:String,
    val age:Int
){

    /*
    Factory라는 객체 이름을 붙일수도 있고
    Log와 같이 인터페이스를 구현할 수 있다.
     */
    companion object Factory :Log{
        private const val MIN_AGE: Int = 1

        fun newBaby(name: String): Person {
            return Person(name,MIN_AGE)

        }

        override fun log() {
            println("구현한 log 함수")
        }
    }

    fun add(factory: Factory) {
        println(factory.MIN_AGE) // 동행객체 사용가능
    }
}

위의 코드는 컴패니언 객체에 이름을 붙이고, 인터페이스를 구현해본 모습이다.

 

 

 

class Person private constructor(
    val name:String,
    val age:Int
){

    companion object {
        private const val MIN_AGE: Int = 1

        fun newBaby(name: String): Person {
            return Person(name,MIN_AGE)
        }
    }
}
public class Prac {

    public static void main(String[] args) {
        Person.Companion.newBaby("gil");
    }
}

이름을 지정하지않은 컴패니언 객체를 자바코드에서 사용하는 모습이다.

동행객체에 Companion이라는 이름이 자동으로 지정되어있는것을 확인할 수 있다.

 

 

자바의 static 함수처럼 클래스명.함수() 모습으로 사용하고 싶다면 

class Person private constructor(
    val name:String,
    val age:Int
){

    companion object {
        private const val MIN_AGE: Int = 1

        @JvmStatic // 자바의 Static 함수처럼 사용하고싶다면 붙인다.
        fun newBaby(name: String): Person {
            return Person(name,MIN_AGE)
        }
    }
}
public class Prac {

    public static void main(String[] args) {
        Person.Companion.newBaby("gil");
        Person.newBaby("gil");
    }
}

@JvmStatic이라는 어노테이션을 이용하여 컴패니언 오브젝트의 이름을 생략가능하다.

 

 

이름을 지정한다면

class Person private constructor(
    val name:String,
    val age:Int
){

    companion object Factory{
        private const val MIN_AGE: Int = 1

        @JvmStatic // 자바의 Static 함수처럼 사용하고싶다면 붙인다.
        fun newBaby(name: String): Person {
            return Person(name,MIN_AGE)
        }
    }
}
public class Prac {

    public static void main(String[] args) {
        Person.Factory.newBaby("gil");
        Person.newBaby("gil");
    }
}

지정한 이름을 가지고 접근가능하다.

 

companion object에 유틸성 함수를 넣고 @JvmStatic을 이용하여 static 함수처럼 사용하는것보다는

파일 최상위에 메소드를 넣고 사용하는것이 더 좋다.

 

그 이유

파일 최상위 에 유틸리티 함수를 선언하면 코드가 더 간단하고 직관적입니다.

  • companion object와 같은 추가적인 구조가 필요 없으며, 함수만 정의하면 됩니다.
  • 함수 호출 시 클래스 이름이나 객체를 통한 접근이 필요 없으므로 사용법이 간단합니다.
  • 파일 최상위 방식은 호출 시 클래스 이름이 필요 없어 더 간단합니다.

Kotlin에서 파일 최상위에 정의된 함수는 JVM 수준에서 static 함수로 생성됩니다.

  • 즉, @JvmStatic을 사용하지 않아도 자연스럽게 static 함수처럼 동작합니다.
  • 따라서 @JvmStatic을 사용하는 companion object 접근 방식보다 추가적인 어노테이션 없이도 동일한 성능을 얻을 수 있습니다.

유틸리티 함수는 특정 클래스의 책임에 속하지 않으므로, 클래스 내부에 포함하지 않는 것이 더 적합합니다.

  • companion object에 유틸리티 함수를 두면 해당 클래스에 속하는 기능으로 오해할 수 있습니다.
  • 반면, 파일 최상위 함수는 유틸리티 함수가 특정 클래스에 의존하지 않고 독립적으로 설계되었음을 명확히 보여줍니다.

 

 


2. 싱글톤

 

자바에서 싱글톤 코드는 다음과 같다.

public class SingletonJava {
    @Getter // 생성되는 getter 메서드는 필드의 static 여부를 따라간다.
    private static final SingletonJava INSTANCE = new SingletonJava();

    private SingletonJava() {}

}


class abc{
    public static void main(String[] args) {
        SingletonJava instance = SingletonJava.getINSTANCE();
    }
}

 

 

 

코틀린에서 싱글톤 코드는 다음과 같다.

// object 키워드를 이용해서 싱글톤 클래스를 만들 수 있다.
object Singleton{
    var a:Int = 0
}

fun main() {
    println(Singleton.a)
    Singleton.a += 10
    println(Singleton.a)
}

 

 


3. 익명 클래스

 

 

특정 인터페이스나 클래스를 상속받은 구현체를 일회성으로 사용할 때 쓰는 클래스

 

자바 예시)

public interface Movable {

  void move();

  void fly();

}





public class Lec12Main {

  public static void main(String[] args) {
    moveSomething(new Movable() {
      @Override
      public void move() {
        System.out.println("move");
      }

      @Override
      public void fly() {
        System.out.println("fly");
      }
    });
  }


  public static void moveSomething(Movable movable) {
    movable.move();
    movable.fly();
  }

}

 

moveSomething() 를 사용할때 일회성으로 Movable를 구현해서 전달해주는데

그 일회성 객체가 익명 객체이다.

 

{
      @Override
      public void move() {
        System.out.println("move");
      }

      @Override
      public void fly() {
        System.out.println("fly");
      }
}

이 부분이 익명 클래스 구현부분이고

 

결국 new Movable()해서 생성된것이 익명객체이다.

 

 

 

코틀린으로 익명클래스,익명객체 예시)

 

fun main() {
    // 코틀린에서는 Movable을 상속받은 object를 만드는 느낌으로 익명클래스 구현 및 익명객체 생성을 한다.
    moveSomething(object : Movable{
        override fun fly() {
            println("fly")
        }

        override fun move() {
            println("move")
        }
    })
}

 

코틀린에서는 object : 타입이름 으로 익명 클래스 구현 및 익명객체 생성을 한다. 

 

 

object의 역할

1. 싱글턴 객체를 생성하는 데 사용됩니다. 즉, object로 정의된 클래스는 하나의 인스턴스만 존재합니다.

2. object를 사용하여 익명 객체를 만들 수 있습니다. 이 경우 object는 타입을 지정하고 해당 타입을 구현하는 객체를 생성하는 데 사용됩니다.

 

 

  • 싱글턴 객체로 사용할 때는 object가 클래스의 하나의 인스턴스를 정의합니다.
  • 익명 객체(익명 클래스)를 만들 때는 object가 해당 인터페이스나 클래스를 즉석에서 구현하는 일회성 객체를 생성합니다.

 


 

4. 중첩 클래스

 

중첩클래스에는

Inner Class(내부클래스)와 static이 붙은 중첩클래스 두가지로 나뉜다.

[ 그냥 static이 안붙은 Inner Class를 Static Nested Class로 부르는거 같다. ]

 

 

 

☕ 내부 클래스(Inner Class) 장점 & 종류 총정리

내부 클래스 (Inner Class) 내부 클래스(inner class)란 하나의 클래스 내부에 선언된 또 다른 클래스를 의미한다. 보통 사용자 클래스 자료형이 필요하면, 메인 클래스 외부에 선언하거나, 따로 독립적

inpa.tistory.com

 

결론적으로 내부클래스는 static을 붙여야한다.

[ static nested class는 외부클래스 참조가 없으므로 메모리 누수가 일어나지않는다]

 

코틀린은 위의 규칙을 기본적으로 지킨다.

 

자바에서 내부클래스 예시)

public class JavaHouse {

  private String address;
  private LivingRoom livingRoom;

  public JavaHouse(String address) {
    this.address = address;
    this.livingRoom = new LivingRoom(10);
  }

  public LivingRoom getLivingRoom() {
    return livingRoom;
  }

  public class LivingRoom {
    private double area;

    public LivingRoom(double area) {
      this.area = area;
    }

    public String getAddress() {
      return JavaHouse.this.address;
      /*
      return JavaHouse.address; // address가 static 변수가 아니라서 이런식으로 작성못한다.
      JavaHouse javaHouse = JavaHouse.this; // this는 현재 클래스의 인스턴스를 참조한다.
      즉 어떤 JavaHouse 객체냐는 이 코드가 선언된 위치가 중요하다.
      LivingRoom안에서 JavaHouse.this를 하게되면 LivingRoom 객체와 관련된 JavaHouse 객체를 가져온다.
      즉 내부클래스 객체와 관련된 외부클래스 객체를 가져온다.

      그래서
      LivingRoom에서 JavaHouse.this.address;를 하게되면
      현재 LivingRoom 객체와 관련있는 JavaHouse 클래스의 인스턴스 변수를 참조해서 address를 가져온다.

       */
    }
  }

}

 

자바에서 static nested class [ = static 내부 클래스] 예시)

public class JavaHouse {

  private String address;
  private LivingRoom livingRoom;

  public JavaHouse(String address) {
    this.address = address;
    this.livingRoom = new LivingRoom(10);
  }

  public LivingRoom getLivingRoom() {
    return livingRoom;
  }

  public static class LivingRoom {
    private double area;

    public LivingRoom(double area) {
      this.area = area;
    }
    
  }

}

 

 

 

코틀린에서 static 중첩클래스를 만든 예시)

class House(
    private val address: String,
    private val livingRoom: LivingRoom
) {
    class LivingRoom(
        private val area: Double
    )
}

 

코틀린에서 권장되는 중첩클래스(static 클래스)를 사용할때는 위와 같이 코드를 짜면된다.

그냥 class 키워드를 이용해서 내부에 클래스를 구현해주면 된다.

[ 기본적으로 코틀린의 내부클래스는 외부클래스와 연관이 없게끔 설계된다. ==> static nested class ]

 

 

코틀린에서 static 사용하지않는 중첩 클래스를 만든 예시)

class House(
    private val address: String,
    private val livingRoom: LivingRoom
) {
    inner class LivingRoom(
        private val area: Double
    ){
        val address:String
            get() = this@House.address
        /*
        코틀린에서 외부클래스의 프로퍼티에 접근하려면
        this@외부클래스명.프로퍼티명 이런식으로 접근한다.
         */
    }
}

코틀린에서 권장되지않은 내부클래스를 만들었을때 코드이다.

 

inner라는 키워드를 붙여서 내부클래스를 생성 가능하다.

 

내부 클래스가 외부클래스를 참조하므로 내부 클래스에서 외부클래스의 프로퍼티를 접근하려고 할때

this@외부클래스명.프로퍼티명 이런식으로 참조가 가능하다.

 

 

코틀린에서는 기본적으로 외부 클래스 참조하지 않는다. 외부 클래스를 참조하고 싶다면 내부 클래스에 inner 키워드를 추가한다.

 


 

5. Data Class

 

계층간의 데이터를 전달하기 위한 DTO(Data Transfer Object)가 존재하고

 

DTO에는 필드, 생성자뿐만 아니라

getter setter,

equals,

hashCode,

toString

등이 들어갈 수 있다.

 

 

자바에서 DTO 코드)

public class JavaPersonDto {

  private final String name;
  private final int age;

  public JavaPersonDto(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public String getName() {
    return name;
  }

  public int getAge() {
    return age;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    JavaPersonDto that = (JavaPersonDto) o;
    return age == that.age && Objects.equals(name, that.name);
  }

  @Override
  public int hashCode() {
    return Objects.hash(name, age);
  }

  @Override
  public String toString() {
    return "JavaPersonDto{" +
        "name='" + name + '\'' +
        ", age=" + age +
        '}';
  }
}

자바에서 DTO를 구현할때

 

IDE의 도움을 받아 편하게 코드를 작성하거나 Lombok 어노테이션을 이용할 수도 있지만

 

그래도 클래스가 장황해지거나 추가적인 처리를 해줘야한다는 단점이 있다.

 

위의 클래스를 코틀린에서 작성해보면 다음과 같다.

 

 

코틀린 코드)

data class PersonDto(
    val name:String,
    var age:Int
)

fun main() {
    // named argument를 이용하면 builder처럼 생성도 가능
    val personDto = PersonDto(
        name = "gil",
        age = 10
    )
}

 

PersonDto( 프로퍼티들) 의 코드로 생성자,getter,setter는 만들어지고

 

data 키워드로 인해 equals, hashcode,toString이 만들어진다.

 

 

Java에서는 JDK16부터 Kotlin의 data class 같은 record class를 도입했다.

 

 

6. Enum Class

 

 

자바 코드 예시)

public enum JavaCountry {

  KOREA("KO"),
  AMERICA("US");

  private final String code;

  JavaCountry(String code) {
    this.code = code;
  }

  public String getCode() {
    return code;
  }

}

추가적인 클래스를 상속받을 수 없다. 인터페이스는 구현할 수 있으며, 각 코드가 싱글톤이다.

 

 

위의 자바 enum 클래스를 코틀린으로 작성해보면 다음과 같다.

enum class Country(
    private val code:String
){
    KOREA("KO"),
    AMERICA("US");
}

 

 

 

when은 Enum Class 혹은 Sealed Class와 함께 사용할 경우, 더욱더 진가를 발휘한다 라고 했었는데

 

when사용하지 않은 enum 이용 자바 코드 예시)

private static void handleCountry(JavaCountry country) {
  if (country == JavaCountry.KOREA) {
    // 로직 처리
  }

  if (country == JavaCountry.AMERICA) {
    // 로직 처리
  }
}

여러 if문의 분기를 만들거나, switch-case를 이용하여 Enum값마다 동작하는 코드를 작성해줘야하니까 코드 복잡성이 올라간다.

 

 

하지만 when을 이용하면 코틀린 코드는 다음과 같다.

 

fun handleCountry(country: Country) {
    when (country){
        Country.KOREA -> println("KO")
        Country.AMERICA -> println("AM")
    }
}

 

 

 

enum 클래스는 컴파일 타임에 정의된 고정된 상수들의 집합으로 취급됩니다.

즉, enum 클래스는 그 값들이 컴파일 시에 명확히 정해져 있으며,

코드 작성 시 그 값들을 사용한다고 선언함으로써 컴파일러가 그것을 인식할 수 있게 됩니다.

 

그래서 when 분기문중 else가 없어도 컴파일 에러가 발생하지않는다.

[ 즉 넘겨받은 enum 객체는 분명 Enum 클래스안에 정의된 객체들 중 하나일 수 밖에 없다는걸 확신할 수 있으니까 ]

 

 

 

 

7. Sealed Class, Sealed Interface

 

 

지정한 클래스만 상속받을 수 있도록 하는 키워드 sealed

 

컴파일 타임 때 하위 클래스의 타입을 모두 기억한다.

 

즉, 런타임때 클래스 타입이 추가될 수 없다 그리고 하위 클래스는 같은 패키지에 있어야 한다.

 

 

open class otherClass

// sealed class도 다른 클래스를 상속 할 수 있다.
sealed class Hyundai(
    val name: String,
    val price: Int,
) : otherClass()

class Avante : Hyundai("Avante",1000)
class Sonata : Hyundai("Sonata",1000)

 

컴파일 타임 때 하위 클래스의 타입을 모두 기억한다. 즉, 런타임때 클래스 타입이 추가될 수 없다.

 

중요한 점은 sealed 클래스의 하위 클래스들은 같은 파일 내에서만 정의되어야 한다는 점이다.

 

같은 파일안에 모든 코드가 쓰여져야한다.

 

 

컴파일 타임 때 하위 클래스 타입을 모두 기억 하기 때문에

위에 enum에서 when을 활용했을때 가지는 장점을 sealed class에서도 사용할 수 있다.

[ else 를 구현안해도된다. ] 

 

 

 

추상화가 필요한 Entity or DTO에 sealed class를 활용하면 좋다.

 

 

추가로, JDK17 에서도 Sealed Class가 추가되었다.

[문법은 좀 다르다고 한다. ]

 

댓글