[Kotlin] Modifiers order

Hood·2025년 4월 18일

Kotlin

목록 보기
17/18
post-thumbnail

✍ 코틀린과 친해지자

공부가 필요하다고 느낀 문법을 정리한 글입니다.


들어가기 전

Kotlin의 클래스, 함수, 변수, 프로퍼티, 생성자 앞에는
각 선언의 의미를 더 분명하게 만들어 주는 키워드가 붙을 수 있습니다.
이 키워드들을 수정자(Modifier) 라고 합니다.

수정자는 접근 범위를 제한하기도 하고,
상속 가능 여부를 정하기도 하며,
함수나 프로퍼티의 동작 방식을 바꾸는 역할도 합니다.

이번 글에서는 Kotlin에서 자주 사용하는 수정자들을 정리해보려고 합니다.


수정자란?

수정자는 말 그대로 선언에 추가 의미를 부여하는 키워드입니다.
예를 들어 public, private는 어디까지 접근할 수 있는지를 나타내고,
open, abstract는 상속과 재정의 가능 여부를 나타냅니다.

즉, 수정자는 단순한 문법이 아니라
코드의 의도와 사용 범위를 더 명확하게 드러내는 도구라고 볼 수 있습니다.


1. 접근 제어자 (Visibility Modifiers)

접근 제어자는 코드가 어디까지 공개되는지를 정할 때 사용합니다.

접근 제어자의미주로 언제 사용할까?
public어디서든 접근 가능외부에서도 자유롭게 사용해야 할 때
private선언된 범위 내부에서만 접근 가능외부에 숨기고 싶은 구현일 때
protected클래스 내부와 하위 클래스에서 접근 가능상속 관계에서만 열어두고 싶을 때
internal같은 모듈 안에서만 접근 가능모듈 내부 공유용으로 둘 때

public

Kotlin에서 별도로 접근 제어자를 쓰지 않으면 기본은 public입니다.

public class Animal(val name: String)

외부 클래스나 다른 파일에서도 접근할 수 있는 가장 기본적인 접근 범위입니다.


private

private은 외부에 노출하지 않고, 내부 구현으로만 감추고 싶을 때 사용합니다.

class Animal {
    private val secret = "숨겨진 값"
}

secretAnimal 클래스 바깥에서는 접근할 수 없습니다.


protected

protected는 클래스 내부와 그 클래스를 상속한 하위 클래스에서만 사용할 수 있습니다.

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

class Dog : Animal() {
    fun bark() {
        sound() // 사용 가능
    }
}

protected는 상속 관계에서만 열어두고 싶을 때 유용합니다.


internal

internal은 같은 모듈 안에서만 접근할 수 있도록 제한합니다.

internal fun doSomething() {}

fun main() {
    doSomething()
}

프로젝트 내부에서는 공유하지만, 외부 라이브러리 사용자에게는 숨기고 싶을 때 자주 사용합니다.


2. 클래스 관련 Modifier

이번에는 클래스 선언 앞에 붙는 수정자들을 보겠습니다.

Modifier의미언제 사용할까?
open상속 가능다른 클래스가 상속할 수 있게 열어둘 때
final상속 불가능기본값, 더 이상 상속되지 않게 막고 싶을 때
abstract직접 객체 생성 불가, 하위 클래스가 구현 필요공통 틀만 정의하고 구현은 자식에게 맡길 때
sealed상속 가능한 타입을 제한분기 가능한 타입 집합을 제한하고 싶을 때
inner외부 클래스 참조 가능한 내부 클래스내부 클래스에서 바깥 클래스에 접근해야 할 때
data데이터 중심 클래스DTO, 값 객체, 모델 클래스 작성 시
enum열거형 클래스정해진 상수 묶음을 표현할 때
annotation어노테이션 클래스메타데이터를 정의할 때

open / override

Kotlin의 클래스와 함수는 기본적으로 final입니다.
즉, 아무것도 붙이지 않으면 상속이나 재정의가 불가능합니다.

open class Animal {
    open fun speak() = println("동물 소리")
}

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

상속을 허용하려면 open,
부모의 멤버를 재정의하려면 override를 사용합니다.


abstract

abstract는 공통 구조만 정의하고, 실제 구현은 하위 클래스가 하도록 맡기는 방식입니다.

abstract class Shape {
    abstract fun area(): Double
}

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

abstract class는 직접 인스턴스를 만들 수 없고,
자식 클래스가 반드시 필요한 부분을 구현해야 합니다.


sealed

sealed는 상속 가능한 타입을 제한할 때 사용합니다.
보통 결과 상태나 화면 상태처럼 정해진 경우만 존재해야 하는 타입을 만들 때 유용합니다.

sealed class Result

class Success(val data: String) : Result()
class Error(val message: String) : Result()

sealed class를 사용하면 when 분기에서 가능한 경우를 더 안전하게 다룰 수 있습니다.


inner

중첩 클래스가 바깥 클래스의 프로퍼티나 함수에 접근해야 한다면 inner를 사용합니다.

class Outer {
    private val outerName = "Outer"

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

fun main() {
    Outer().Inner().print() // Outer
}

inner가 붙은 내부 클래스는 외부 클래스 인스턴스를 참조할 수 있습니다.


data

data class는 데이터를 담기 위한 클래스입니다.
자동으로 equals(), hashCode(), toString() 등이 생성됩니다.

data class User(val id: Int, val name: String)

값 비교나 출력이 자주 필요한 객체에서 매우 자주 사용됩니다.


enum

enum class는 정해진 상수 집합을 표현할 때 사용합니다.

enum class Direction(val desc: String) {
    NORTH("북"),
    SOUTH("남"),
    EAST("동"),
    WEST("서")
}

방향, 상태, 권한처럼 선택지가 고정된 경우에 잘 어울립니다.


annotation

어노테이션은 코드에 메타데이터를 붙이는 방법입니다.

annotation class MyAnnotation

@MyAnnotation
fun hello() {
}

프레임워크나 라이브러리에서 특정 동작을 연결할 때 자주 사용됩니다.


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

이번에는 함수와 프로퍼티에 붙는 수정자들입니다.

Modifier의미언제 사용할까?
override부모 멤버 재정의상속받은 기능을 다시 구현할 때
lateinit나중에 초기화할 프로퍼티DI, 테스트, 지연 초기화가 필요할 때
const컴파일 타임 상수전역 상수 정의 시
suspend코루틴에서 중단 가능한 함수비동기 작업 처리 시
tailrec꼬리 재귀 최적화재귀를 더 안전하게 작성할 때
infix중위 표기 허용함수 호출을 자연스럽게 표현할 때
operator연산자 오버로딩+, [] 같은 연산자 정의 시
external외부 네이티브 구현 사용JNI, 네이티브 코드 연결 시
inline함수 본문 인라인고차 함수 오버헤드 감소가 필요할 때
noinline인라인 제외 람다inline 함수 안에서 특정 람다만 제외할 때
crossinline비지역 return 금지inline 람다의 return을 제한할 때
vararg가변 인자인자 개수가 일정하지 않을 때
get, set접근자 커스터마이징프로퍼티 읽기/쓰기 로직을 직접 제어할 때

lateinit

lateinit은 “지금은 초기화하지 않지만, 나중에 반드시 값을 넣겠다”는 의미입니다.

class Service {
    lateinit var config: String
}

lateinitvar에만 사용할 수 있으며,
nullable로 만들고 싶지 않지만 즉시 초기화가 어려운 경우에 유용합니다.


const

const는 컴파일 시점에 값이 확정되는 상수입니다.

const val PI = 3.14159

constval과 비슷하지만,
top-level이나 object, companion object 안에서만 사용할 수 있습니다.


suspend

suspend는 코루틴 안에서 실행할 수 있는 함수입니다.

suspend fun fetchData(): String {
    return "Data"
}

suspend는 “비동기 함수”라기보다
중단 가능한 함수라고 이해하는 편이 더 정확합니다.


tailrec

tailrec은 꼬리 재귀를 최적화할 때 사용합니다.

tailrec fun factorial(n: Int, acc: Int = 1): Int =
    if (n <= 1) acc else factorial(n - 1, n * acc)

재귀 호출이 마지막 연산일 때,
컴파일러가 반복문처럼 최적화할 수 있게 도와줍니다.


infix

infix는 함수를 중위 표기법으로 호출할 수 있게 합니다.

infix fun Int.add(x: Int): Int = this + x

fun main() {
    val result = 3 add 5
    println(result) // 8
}

a to b 같은 문법도 이런 방식으로 동작합니다.


operator

operator는 연산자 오버로딩에 사용됩니다.

data class Price(val value: Int) {
    operator fun plus(other: Price): Price {
        return Price(value + other.value)
    }
}

fun main() {
    val a = Price(10)
    val b = Price(50)
    val result = a + b
    println(result.value) // 60
}

연산자 문법을 클래스에 맞게 자연스럽게 정의할 수 있습니다.


external

external은 함수 본문이 Kotlin에 없고, 외부 네이티브 코드에 구현되어 있음을 나타냅니다.

external fun nativeMethod(): Int

주로 JNI처럼 C/C++ 네이티브 코드와 연결할 때 사용합니다.
Java 메서드 호출을 의미하는 것은 아니라는 점을 구분해두면 좋습니다.


inline / noinline / crossinline

이 세 가지는 고차 함수에서 자주 등장합니다.

  • inline : 함수 본문을 호출 위치에 삽입
  • noinline : 특정 람다는 인라인하지 않음
  • crossinline : 비지역 return을 막음
inline fun runAction(action: () -> Unit) {
    action()
}

이 부분은 람다와 고차 함수를 어느 정도 익힌 뒤 보면 더 이해가 잘 됩니다.
나중에 따로 다뤄도 좋은 주제입니다.


vararg

vararg는 개수가 정해지지 않은 인자를 받을 수 있게 해줍니다.

fun sum(vararg numbers: Int): Int = numbers.sum()

fun main() {
    val total = sum(1, 2, 3, 4)
    println(total) // 10
}

get / set

Kotlin 프로퍼티는 기본적으로 getter, setter를 가집니다.
필요하면 직접 커스터마이징할 수 있습니다.

class UserInfo {
    var address: String = ""
        set(value) {
            field = if (value == "한국") "대한민국" else value
        }
        get() = "${field}에 거주하고 있습니다"
}

fun main() {
    val user = UserInfo()
    user.address = "한국"
    println(user.address) // 대한민국에 거주하고 있습니다
}

field는 실제 저장되는 backing field를 의미합니다.


4. 객체 관련 Modifier

Modifier의미언제 사용할까?
object싱글턴 객체 선언인스턴스가 하나만 필요할 때
companion object클래스 내부의 정적 멤버 비슷한 객체Java의 static처럼 사용하고 싶을 때

object

object는 싱글턴 객체를 만들 때 사용합니다.

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

프로그램 전체에서 하나만 존재해야 하는 객체를 간단하게 만들 수 있습니다.


companion object

companion object는 클래스 내부에서 정적 멤버처럼 사용할 수 있는 객체입니다.

class MyClass {
    companion object {
        const val VERSION = "1.0"
        fun create() = MyClass()
    }
}
fun main() {
    println(MyClass.VERSION)
    val obj = MyClass.create()
}

Java의 static과 비슷한 느낌으로 사용할 수 있습니다.


📌 결론

이번 글에서는 Kotlin의 수정자(Modifier)를 정리해보았습니다.

수정자는 단순히 문법을 꾸미는 키워드가 아니라,
코드의 공개 범위와 상속 구조, 함수 동작 방식까지 결정하는 중요한 요소입니다.

정리하면 다음과 같이 볼 수 있습니다.

  • 접근 제어자 : public, private, protected, internal
  • 클래스 관련 : open, abstract, sealed, data, enum, annotation, inner
  • 함수/프로퍼티 관련 : override, lateinit, const, suspend, tailrec, infix, operator, external, inline, vararg, get, set
  • 객체 관련 : object, companion object

기본기가 탄탄해야 코드를 더 의도적으로 작성할 수 있습니다.
수정자도 하나씩 익혀두면, 나중에는 문법이 아니라 코드의 설계 의도를 표현하는 도구로 보이기 시작합니다.

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

0개의 댓글