Developing Myself Everyday

클래스 관계


클래스 간의 관계는 아래와 같습니다.


 상속관계(is a):
클래스를 상속해서 하나의 클래스처럼 사용
 연관관계(has a): 클래스를 상속하지 않고 내부적인 속성에 객체를 만들어서 사용
 결합관계(약한 has a): 연관관계를 구성하는 방식 중에 클래스 간의 주종관계 없이 단순하게 사용하는 관계
 조합관계(강한 has a): 연관관계를 구성하는 방식 중에 클래스 간의 주종관계가 있어서 분리할 수 없는 관계
 의존관계(사용 has a): 필요한 클래스를 매개변수로 받아 필요한 시점에 사용하는 관계

 

상속관계 (is-a)

open class Animal(val name: String)

class Dog(name: String) : Animal(name)

class Cat(name: String) : Animal(name)

 

 

연관관계 (has-a)

class Engine(val type: String)

class Car(val name: String, val engine: Engine)

 

결합관계 (약한 has-a)

class Person(val name: String)

class Team(val members: List<Person>)

 

조합관계 (강한 has-a)

class Department(val name: String, val employees: List<Employee>)

class Employee(val name: String, val department: Department)

 

 

의존관계 (사용 has-a)

class Logger {
    fun log(message: String) {
        println("Logging: $message")
    }
}

class UserManager(val logger: Logger) {
    fun addUser(username: String) {
        logger.log("Added user: $username")
        // 사용자를 추가하는 로직
    }
}

 

 

 

세터 비공개 처리


특정 프로퍼티에 대한 읽기 전용 접근을 제공하면서 쓰기 접근을 특정 메서드나 클래스에 제한해야 할 때 `private set`를 사용할 수 있습니다.

 

만약 사용자가 특정 프로퍼티를 직접 업데이트할 수 있게 된다면, 값이 잘못된 상태가 될 가능성이 있습니다. 그렇기에 프로퍼티에 비공개를 정의하고 클래스 외부에서 읽기 전용으로 들면 됩니다.

class Example {
    var cnt: Int = 0
        private set

    fun counter() = cnt++
}

fun main() {
    val example = Example()
    repeat(4) { example.counter() }
    println(example.cnt) // 출력: 4
}

이는 프로퍼티 자체를 `private`로 설정하고 값을 읽을 수 있는 메서드를 추가하는 것보다 더 간결하고 가독성이 뛰어납니다.

 

 

`private set`은 프로퍼티가 변경될 때마다 특정 로직을 실행하고 싶을 때에도 유용합니다.

class Example {
    var cnt: Int = 0
        private set(value) {
            print("$value ")
            field = value
        }

    fun counter() = cnt++
}

fun main() {
    val example = Example()
    repeat(4) { example.counter() }
    println(example.cnt) 
}
// 출력: 1 2 3 4 4

이렇게 하면 이제 cnt가 변경될 때마다 값을 출력해 확인할 수 있습니다.

 

 

 

Infix(인픽스)


Infix 함수는 두개의 변수 가운데 오는 함수를 말합니다. 코틀린에서 기본적으로 정의된 Infix 함수들 중에 Pair를 만드는 to가 있습니다.

 

val pair : Pair<String, Int> = "White" to 10

 

`infix` 예약어를 사용하면 메서드나 확장 함수를 infix로 사용할 수 있습니다.

infix fun Int.add(x: Int): Int {
    return this + x
}

fun main() {
    val result = 5 add 3 // 일반 함수 호출처럼 보이지만 `add`가 `infix` 함수로 호출됨
    println(result) // 출력: 8
}

 

 

 

데이터 클래스


코틀린에서 데이터 클래스(data class)는 데이터를 저장하고 전달하기 위한 목적으로 사용되는 특별한 종류의 클래스입니다. 데이터 클래스는 일반 클래스와는 다른 방식으로 동작하며, 주로 데이터를 담는 용도로 사용됩니다. 데이터 클래스는 보통 toString(), hashCode(), equals(), copy() 메서드를 자동으로 생성해줍니다.

 

 

 

 

이넘 클래스


이넘(enum)열거형(enumeration)의 줄임말로, 서로 연관된 상수의 집합을 나타내는 데이터 형식입니다.

 

이넘의 상수는 대문자로 작성합니다.

 

enum class Color {
    RED, GREEN, BLUE
}

fun main() {
    val selectedColor: Color = Color.GREEN

    when (selectedColor) {
        Color.RED -> println("Selected color is red")
        Color.GREEN -> println("Selected color is green")
        Color.BLUE -> println("Selected color is blue")
    }
}

이넘 클래스는 컴파일러가 이넘 클래스의 모든 상수를 알고 있고, 상수의 경우에 따라 브랜치를 매칭시키기 때문에 `else`를 사용하지 않아도 됩니다.

 

 

각각의 이넘 상수들은 이넘 클래스의 인스턴스입니다. 그렇기에 특정 값으로 초기화될 수 있습니다.

enum class Status(val label: String) {
    ACTIVE("Active"),
    INACTIVE("Inactive"),
    PENDING("Pending")
}

fun main() {
    val status: Status = Status.ACTIVE
    println(status) // 출력: Active
}

 

또한 내부에 메서드를 넣을 수 도 있습니다. 다만 이때는 멤버 정의 뒤에 (;)를 넣어 구분해야 합니다.

enum class Status(val label: String) {
    ACTIVE("Active"),
    INACTIVE("Inactive"),
    PENDING("Pending");

    fun printLabel() {
        println("Status: $label")
    }
}

fun main() {
    val status: Status = Status.ACTIVE
    status.printLabel() // 출력: Status: Active
}

 

 

추상 클래스를 이넘 클래스 내부에 정의하고 이를 멤버가 override 할 수 있습니다.

enum class Status {
    ACTIVE {
        override fun printLabel() {
            println("현재 상태는 ACTIVE입니다.")
        }
    },
    INACTIVE {
        override fun printLabel() {
            println("현재 상태는 INACTIVE입니다.")
        }

    },
    PENDING {
        override fun printLabel() {
            println("현재 상태는 PENDING입니다.")
        }

    };

    abstract fun printLabel()
}

fun main() {
    val status: Status = Status.ACTIVE
    status.printLabel() // 출력: 현재 상태는 ACTIVE입니다.
}

 

 

 

 

인라인 클래스


인라인 클래스(inline class)는 Kotlin 1.3 버전부터 도입된 기능으로, 새로운 유형의 클래스를 정의하는 방법입니다. 이 클래스는 런타임에 별도의 객체를 생성하지 않으며, 컴파일 타임에 해당 클래스가 사용된 곳에 클래스의 필드만 삽입되는 형태로 동작합니다. 이로써 성능상의 이점과 가독성을 모두 취할 수 있습니다.

 

`inline`를 사용하는 방식은 Deprecated 되었기에 value 키워드@JvmInline 어노테이션을 사용해야 합니다.

@JvmInline
value class Name(val value: String)

 

그래도 이해를 위해 inline 키워드를 사용한 예시를 보겠습니다. 

fun fn(n1: Int, n2: Int): Int {
    return n1 + n2
}

fun main() {
    val result = fn(1, 2)
    println(result)
}

위의 코드를 자바로 변환하면 아래와 같습니다.

public static final int fn(int n1, int n2) {
   return n1 + n2;
}

public static final void main() {
   int result = fn(1, 2);
   System.out.println(result);
}

인라인이 아닌 일반 함수는 main에서 fn을 호출해 결과를 받고 있습니다.

 

이제는 inline을 사용한 코드의 자바 코드를 보겠습니다.

public static final int fn(int n1, int n2) {
   int $i$f$fn = 0;
   return n1 + n2;
}

public static final void main() {
   byte n1$iv = 1;
   int n2$iv = 2;
   int $i$f$fn = false;
   int result = n1$iv + n2$iv;
   System.out.println(result);
}

인라인일 때는 fn 에서 하는 일을 main 에서 인라인되어 실행시켜 줌을 확인할 수 있습니다. 코틀린에서는 보통 위와 같이 단순한 경우에는 inline을 사용할 필요가 없고 함수형 인자를 받아 함수에서 실행시켜 줄 때나 건내줄 때 inline이 성능적으로 많이 개선이 됩니다.

 

 

 

 

인라인 클래스의 필요성


만약 하나의 타입을 만들어 그 타입만 가질 수 있는 여러가지 동작을 정의하고 싶을 때 사용하는 방법들이 있습니다.

 

 

타입별칭(typealias)

타입별칭은 기존 데이터 타입에 대한 다른 이름(alias)을 만드는 방식입니다. 즉, 같은 타입에 대해 다른 이름을 붙이는 것입니다.

typealias Fruit = String

fun printFruit(name: Fruit) {
    println("과일 이름: $name")
}
fun main() {
    val apple : Fruit = "사과"
    printFruit(apple) // 출력: 과일 이름: 사과
}

 

다만 아래와 같이 Fruit 타입이 아닌 String 타입에서도 `printFruit`를 호출할 수 있습니다.

printFruit("바나나") // 출력: 과일 이름: 바나나

typealias는 새로운 타입을 만드는 것이 아닌 String에 또 다른 이름을 붙인 것에 불과하기 때문입니다.

 

실제 자바 코드로 변환된 코드를 보면 String을 그냥 사용하고 있습니다.

public static final void main() {
   String apple = "사과";
   printFruit(apple);
}

 

 

 

Wrapper class

Wrapper class를 사용하면 Fruit가 아닌 타입에서 `printFruit`를 호출할 수 없을 것입니다.

class Fruit(private val name: String) {
    fun printFruit() {
        println("과일 이름: $name")
    }
}

fun main() {
    val fruit = Fruit("사과")
    fruit.printFruit() // 출력: 과일 이름: 사과
}

 

다만 디컴파일된 자바 코드를 보면 new 생성자를 사용해 객체를 생성해주기 때문에 최적화된 방법이 아닙니다.

public static final void main() {
  Fruit fruit = new Fruit("사과");
  fruit.printFruit();
}

 

 

 

Inline class

이제 인라인 클래스를 사용한 코드를 보겠습니다.

@JvmInline
value class Fruit(private val name: String) {
    fun printFruit() {
        println("과일 이름: $name")
    }
}

fun main() {
    val fruit = Fruit("사과")
    fruit.printFruit() // 출력: 과일 이름: 사과
}

 

그리고 디컴파일된 자바 코드를 보겠습니다.

public static final void main() {
  String fruit = Fruit.constructor-impl("사과");
  Fruit.printFruit-impl(fruit);
}

 

 

놀랍게도 Fruit 자료형은 실제로 쓰이지않고 기존 Wrapper class에서 사용되던 함수들이 static 함수로 정의된 헬퍼 클래스로 변했습니다. 그 증거로 Fruit 에서 constructor-impl, printFruit-impl 같은 유틸리티 함수들이 쓰이고 String 자료형이 그대로 사용되었습니다.

 

이처럼 인라인 클래스는 코드를최적화해주며 새로운 타입을 만들어내고 안전한 사용법을 강제할 수 있습니다.

 

 

 

Reference

 

코틀린 인라인 클래스란? 💍

코틀린의 인라인 클래스의 필요성과 사용법

medium.com

 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!