[Kotlin] Modifiers order

Hood·2025년 4월 18일

Kotlin

목록 보기
17/18
post-thumbnail

✍  코틀린과 친해지자

공부가 필요한 문법에 대해서 정리한 글입니다.


들어가기 전

코틀린의 클래스, 함수, 변수, 프로퍼티, 생성자 등에는 앞에 부과적인 의미를 부여하는 키워드가 존재합니다.
이번 포스트에서는 모든 수정자(Modifiers)를 정의해보려고 합니다.


공식 문서 속 수정자

코틀린 공식문서에 따르면 다음과 같은 수정자들이 존재합니다.
접근 범위 제어(public, private)부터 상속 여부(open, final), 가시성(internal) 등을 표현할 수 있는 다양한 키워드가 존재합니다.

참고

1. 접근제어자 (Visibility Modifiers)

접근제어자 는 간단하게 말하면 코드의 접근 범위를 지정할 때 사용됩니다.

접근 제어자사용 기능 위치접근 범위언제 사용하나?
public어디서든모든 곳에서 접근 가능외부 모듈/클래스에서도 사용할 수 있도록 할 때
private클래스, 파일, 블록 내부정의된 범위 내에서만 접근 가능외부에 노출시키고 싶지 않은 내부 구현일 때
protected클래스 내부 + 하위 클래스외부에서는 접근 불가상속 관계에서만 접근 가능하게 하고 싶을 때
internal같은 모듈 내같은 모듈에서만 접근 가능모듈 내부에선 공유하지만, 외부에선 숨기고 싶을 때

public

//어디서든 사용 가능한 클래스 (선언 안해도 자동으로 public 클래스임.)
public class Animal(val name: String)

private

//secret는 Animal 클래스 외부에서는 접근 불가
class Animal {
    private val secret = "숨겨진 값"
}

protected

open class Animal {
    protected fun sound() { println("울음 소리!") }
}

//상속받은 클래스 내부에서는 사용 가능
class Dog : Animal() {
    fun walwal() {
        //사용 가능
        sound()
    }
}

class Cat {
    fun miaumiau() {
        //사용 불가
        sound()
    }
}

internal

internal fun doSomething() {}

fun main(){
    //같은 모듈 내에서 사용 가능
    doSomething()
}

2. 클래스 상속 관련 Modifier

클래스의 앞에서 사용됩니다. 용도에 따라 상속을 정의해주며 의미는 다음 표와 같습니다.

Modifier정의언제 사용하나?
open다른 클래스가 상속 가능하도록 허용하위 클래스에서 재정의(override)할 수 있게 하고 싶을 때
final상속 불가능 (기본값)기본 클래스에서 수정되면 안 되는 기능일 때
abstract인스턴스를 만들 수 없고, 반드시 하위 클래스가 구현해야 함템플릿용 클래스를 만들고 구체적인 구현을 위임하고 싶을 때
sealed제한된 클래스 상속만 허용 (같은 파일 내에서만 상속 가능)조건 분기 시 타입을 제한하고 exhaustive when을 쓰고 싶을 때
inner내부 클래스가 외부 클래스 참조 가능중첩 클래스에서 외부 클래스의 필드나 메서드에 접근하고 싶을 때
dataequals, hashCode, toString 등을 자동 생성하는 데이터 전용 클래스DTO, 값 객체, 단순 모델 클래스 만들 때
enum열거형 클래스상수값을 그룹화해서 표현하고 싶을 때
annotation어노테이션 정의커스텀 어노테이션을 정의할 때

open + override

//코틀린은 기본 final이라 open이 없으면 override가 불가합니다.
open class Animal {
    open fun speak() = println("동물 소리")
}

class Cat : Animal() {
    override fun speak() {
        println("야옹!")
    }
}

abstract

//추상 메서드는 하위 클래스에서 구현됩니다. 이를 사용하면 인스턴스를 생성할 수 없습니다.
//또한 추상 클래스는 설계도를 만들어 주면 자식이 알아서 책임진다는 특징이 있습니다.
abstract class Shape {
    abstract fun area(): Double
}

class Circle(val r: Double) : Shape() {
    override fun area() = Math.PI * r * r
}

sealed

// 같은 파일 내에서만 상속 가능합니다.
sealed class Result
class Success(val data: String) : Result()
class Error(val message: String) : Result()

inner

//내부 클래스에서 외부 클래스 참조가 가능합니다.
class Outer {
    val outerName = "Outer"

    inner class Inner {
        fun print() = println(outerName)
    }
}

fun main(){
    val n = Outer().Inner().print() //Outer
}

data

//자동으로 equals, hashCode, toString 메소드가 생성됩니다.
data class User(val id: Int, val name: String)

enum

//멤버의 집합을 열거형 타입인 Enum으로 묶어줄 수 있습니다.
//독립된 특수한 클래스로 구분합니다.
enum class Direction(val desc: String) {
    NORTH("북"), SOUTH("남"), EAST("동"), WEST("서")
}

annotation

//어노테이션은 메타데이터를 코드에 첨부하는 수단입니다.
//지금은 커스텀 어노테이션을 만드는 방법에 대한 코드입니다.
annotation class MyAnnotation

@MyAnnotation
fun main(){
    
}

3. 함수/프로퍼티 관련 Modifier

Modifier정의언제 사용하나?
override부모 클래스의 멤버를 재정의상속받은 함수를 커스터마이징하고 싶을 때
lateinit나중에 초기화될 프로퍼티 (var 전용)의존성 주입, 초기화가 늦게 일어나는 프로퍼티에 사용
const컴파일 타임 상수 (val + top-level, object 내부에서만 가능)변하지 않는 전역 상수를 정의하고 싶을 때
suspend코루틴에서 실행 가능한 함수비동기 처리, 네트워크/IO 등 중단 가능한 작업을 정의할 때
tailrec꼬리 재귀 최적화 함수로 컴파일됨재귀 호출이 깊어질 때 스택 오버플로우 방지하고 싶을 때
infix중위 호출 허용a to b처럼 함수 호출을 자연스럽게 표현하고 싶을 때
operator연산자 오버로딩+, [] 같은 연산자를 커스터마이징하고 싶을 때
external외부(C/C++)에서 정의된 함수JNI, 네이티브 라이브러리를 연결할 때
inline고차 함수를 인라인 처리 (성능 최적화)람다를 사용하는 함수에서 오버헤드를 줄이고 싶을 때
noinlineinline 함수 내부에서 인라인하지 않을 인자에 사용인라인은 안 하고 싶은 특정 람다 인자에
crossinline인라인 함수 내 람다에서 return 금지함수 바깥으로 빠져나가는 것을 방지하고 싶을 때
vararg가변 인자 허용개수 제한 없이 여러 인자를 받을 때 (fun sum(vararg nums: Int))
get, set프로퍼티의 접근자 커스터마이징커스텀 로직이 필요한 경우 (val x get() = ...)

lateinit (지연초기화)

/**
변수를 선언할 때 반드시 초기화가 이뤄지게 되는데 lateinit을 사용하게 되면 언제 초기화가 이루어질지 모릅니다.
즉, 나중에 초기화가 된다는 소리이며 불필요한 nullable을 막아주는 역할을 한다. 또한 var로만 사용가능합니다.
val이 안되는 이유는 lateinit은 내부에서 null로 초기화 한 이후에
들어오는 값으로 변경하는 방식으로 구현되어 있기 때문입니다.
**/
class Service {
    lateinit var config: String
}

const

// 컴파일 타임 상수이며 val과 달리 const는 top-level이나 object 내부에서만 사용가능합니다.
// 변하지 않는 전역 상수를 정의하고 싶을 때 사용합니다.
const val PI = 3.14159

suspend

// 코루틴에서 사용할 수 있는 비동기 함수입니다.
// 특징은 하나의 함수가 다 처리될때 까지 기다리는 것이 아닌 동시에 함수를 처리할 수 있다는 장점이 있습니다.
suspend fun fetchData(): String {
    delay(1000)
    return "Data"
}

tailrec

// 재귀 호출이 깊어질 때 최적화하기 위해 사용됩니다.
tailrec fun factorial(n: Int, acc: Int = 1): Int =
    if (n <= 1) acc else factorial(n - 1, n * acc)

infix

//중위 표기법으로 함수를 더 자연스럽게 사용 가능하게 해줍니다.
infix fun Int.add(x: Int): Int = this + x

val result = 3 add 5  // 8

operator

// +, -, [] 같은 연산자를 커스터마이징할 때 사용됩니다.
data class Price(val value: Int) {
    operator fun plus(b: Price): Price {
        return Price(value + b.value)
    }
}
val a: Price = Price(10)
val b: Price = Price(50)
val result: Price = a + b // 60

external

/**
함수나 클래스의 본문이 Kotlin 코드에 포함되지 않고 외부에서 제공되는 것임을 나타냅니다.
Kotlin에서 Java의 메서드나 클래스를 호출하거나
Java 코드에서 Kotlin의 메서드나 클래스를 호출할 때 사용할 수 있습니다.
**/
external fun nativeMethod(): Int

fun main() {
    val result = nativeMethod()
    println("Result from native method: $result")
}

inline / noinline / crossinline

/**
고차 함수를 최적화하는 키워드입니다.
기본적으로 고차 함수를 사용하면 런타임 패널티가 있기 때문에 함수 구현 자체를 코드에 넣음으로써
오버헤드를 없앨 수 있습니다.
이건 다음 포스트에 한 번 다루도록 하겠습니다.
**/

vararg

//여러 개의 인자를 배열처럼 받을 수 있게 해줍니다.
fun sum(vararg numbers: Int): Int = numbers.sum()

val total = sum(1, 2, 3, 4)  // 10

get / set

/**
코틀린은 변수를 만들어주기만 해도 자동적으로 getter, setter를 내부에서 생성합니다.
val 변수는 set으로 값 변경 불가하니 get만 자동 생성합니다.
그런 get과 set을 커스할 때 사용하는 키워드입니다.
출처 : https://velog.io/@dabin/Kotlin-get-set
**/

class UserInfo {
    var name: String = ""
        set(value) {
            if (value == "김연아")
                field = "${value}는 천재입니다"
            else field=value
        }

    var address: String = ""
        set(value) {
            if (value == "한국") {
                field = "대한민국"
            } else field = value
        }
        get() {
            // 반드시 return 문으로 작성해야 합니다.
            return "${field}에 거주하고 있습니다"
        }

    constructor(name: String, address: String) {
        this.name = name
        this.address = address
    }
}

fun main() {
    val user1 = UserInfo("김연아", "한국")
    println(user1.name)
    //김연아는 천재입니다
    println(user1.address)
    //대한민국에 거주하고 있습니다

    val user2 = UserInfo("홍길동", "서울")
    println(user2.name)
    //홍길동
    println(user2.address)
    //서울에 거주하고 있습니다
}

기타 (객체 관련)

Modifier정의언제 사용하나?
object싱글턴 객체를 정의전역에서 하나만 존재해야 할 객체
companion클래스 내부에서 정적 멤버처럼 사용 가능한 객체static처럼 동작하게 하고 싶을 때 (Java와 호환도 고려)

object

/**
싱글톤 패턴은 생성자가 여러 차례로 호출되더라도 실제로 생성되는 객체는 하나이고
최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴하는 유형의 디자인 패턴을 의미합니다.
데이터를 공유가 쉽기 때문에 메모리, 속도의 측면에서 이점이 많습니다.
간단히 설명하면 싱글톤 패턴은 객체의 인스턴스를 한개만 생성되게 하는 패턴입니다.
**/

object Singleton {
    fun greet() = println("Hello")
}

companion

/**
static 처럼 정적 멤버를 정의할 수 있습니다.
외부에서는 MyClass.VERSION 로 사용합니다.
**/
class MyClass {
    companion object {
        const val VERSION = "1.0"
        fun create() = MyClass()
    }
}

object와 companion을 사용한 싱글톤 패턴을 구현하는 법에 대해서도 나중 포스트로
자세하게 다뤄보겠습니다.


📌 결론

이번 포스트에서는 코틀린 공식문서에 있는 수정자 Modifier를 다뤄보았습니다.
기본기가 탄탄해야 코드를 작성할 때 더욱 효율적으로 작성하는 것 같습니다.
위의 키워드를 사용해 클린 코드에 가까워져 봅시다!

profile
달을 향해 쏴라, 빗나가도 별이 될 테니 👊

0개의 댓글