공부가 필요하다고 느낀 문법을 정리한 글입니다.
Kotlin의 클래스, 함수, 변수, 프로퍼티, 생성자 앞에는
각 선언의 의미를 더 분명하게 만들어 주는 키워드가 붙을 수 있습니다.
이 키워드들을 수정자(Modifier) 라고 합니다.
수정자는 접근 범위를 제한하기도 하고,
상속 가능 여부를 정하기도 하며,
함수나 프로퍼티의 동작 방식을 바꾸는 역할도 합니다.
이번 글에서는 Kotlin에서 자주 사용하는 수정자들을 정리해보려고 합니다.
수정자는 말 그대로 선언에 추가 의미를 부여하는 키워드입니다.
예를 들어 public, private는 어디까지 접근할 수 있는지를 나타내고,
open, abstract는 상속과 재정의 가능 여부를 나타냅니다.
즉, 수정자는 단순한 문법이 아니라
코드의 의도와 사용 범위를 더 명확하게 드러내는 도구라고 볼 수 있습니다.
접근 제어자는 코드가 어디까지 공개되는지를 정할 때 사용합니다.
| 접근 제어자 | 의미 | 주로 언제 사용할까? |
|---|---|---|
public | 어디서든 접근 가능 | 외부에서도 자유롭게 사용해야 할 때 |
private | 선언된 범위 내부에서만 접근 가능 | 외부에 숨기고 싶은 구현일 때 |
protected | 클래스 내부와 하위 클래스에서 접근 가능 | 상속 관계에서만 열어두고 싶을 때 |
internal | 같은 모듈 안에서만 접근 가능 | 모듈 내부 공유용으로 둘 때 |
Kotlin에서 별도로 접근 제어자를 쓰지 않으면 기본은 public입니다.
public class Animal(val name: String)
외부 클래스나 다른 파일에서도 접근할 수 있는 가장 기본적인 접근 범위입니다.
private은 외부에 노출하지 않고, 내부 구현으로만 감추고 싶을 때 사용합니다.
class Animal {
private val secret = "숨겨진 값"
}
secret은Animal클래스 바깥에서는 접근할 수 없습니다.
protected는 클래스 내부와 그 클래스를 상속한 하위 클래스에서만 사용할 수 있습니다.
open class Animal {
protected fun sound() {
println("울음 소리!")
}
}
class Dog : Animal() {
fun bark() {
sound() // 사용 가능
}
}
protected는 상속 관계에서만 열어두고 싶을 때 유용합니다.
internal은 같은 모듈 안에서만 접근할 수 있도록 제한합니다.
internal fun doSomething() {}
fun main() {
doSomething()
}
프로젝트 내부에서는 공유하지만, 외부 라이브러리 사용자에게는 숨기고 싶을 때 자주 사용합니다.
이번에는 클래스 선언 앞에 붙는 수정자들을 보겠습니다.
| Modifier | 의미 | 언제 사용할까? |
|---|---|---|
open | 상속 가능 | 다른 클래스가 상속할 수 있게 열어둘 때 |
final | 상속 불가능 | 기본값, 더 이상 상속되지 않게 막고 싶을 때 |
abstract | 직접 객체 생성 불가, 하위 클래스가 구현 필요 | 공통 틀만 정의하고 구현은 자식에게 맡길 때 |
sealed | 상속 가능한 타입을 제한 | 분기 가능한 타입 집합을 제한하고 싶을 때 |
inner | 외부 클래스 참조 가능한 내부 클래스 | 내부 클래스에서 바깥 클래스에 접근해야 할 때 |
data | 데이터 중심 클래스 | DTO, 값 객체, 모델 클래스 작성 시 |
enum | 열거형 클래스 | 정해진 상수 묶음을 표현할 때 |
annotation | 어노테이션 클래스 | 메타데이터를 정의할 때 |
Kotlin의 클래스와 함수는 기본적으로 final입니다.
즉, 아무것도 붙이지 않으면 상속이나 재정의가 불가능합니다.
open class Animal {
open fun speak() = println("동물 소리")
}
class Cat : Animal() {
override fun speak() {
println("야옹!")
}
}
상속을 허용하려면
open,
부모의 멤버를 재정의하려면override를 사용합니다.
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 class Result
class Success(val data: String) : Result()
class Error(val message: String) : Result()
sealed class를 사용하면when분기에서 가능한 경우를 더 안전하게 다룰 수 있습니다.
중첩 클래스가 바깥 클래스의 프로퍼티나 함수에 접근해야 한다면 inner를 사용합니다.
class Outer {
private val outerName = "Outer"
inner class Inner {
fun print() = println(outerName)
}
}
fun main() {
Outer().Inner().print() // Outer
}
inner가 붙은 내부 클래스는 외부 클래스 인스턴스를 참조할 수 있습니다.
data class는 데이터를 담기 위한 클래스입니다.
자동으로 equals(), hashCode(), toString() 등이 생성됩니다.
data class User(val id: Int, val name: String)
값 비교나 출력이 자주 필요한 객체에서 매우 자주 사용됩니다.
enum class는 정해진 상수 집합을 표현할 때 사용합니다.
enum class Direction(val desc: String) {
NORTH("북"),
SOUTH("남"),
EAST("동"),
WEST("서")
}
방향, 상태, 권한처럼 선택지가 고정된 경우에 잘 어울립니다.
어노테이션은 코드에 메타데이터를 붙이는 방법입니다.
annotation class MyAnnotation
@MyAnnotation
fun hello() {
}
프레임워크나 라이브러리에서 특정 동작을 연결할 때 자주 사용됩니다.
이번에는 함수와 프로퍼티에 붙는 수정자들입니다.
| Modifier | 의미 | 언제 사용할까? |
|---|---|---|
override | 부모 멤버 재정의 | 상속받은 기능을 다시 구현할 때 |
lateinit | 나중에 초기화할 프로퍼티 | DI, 테스트, 지연 초기화가 필요할 때 |
const | 컴파일 타임 상수 | 전역 상수 정의 시 |
suspend | 코루틴에서 중단 가능한 함수 | 비동기 작업 처리 시 |
tailrec | 꼬리 재귀 최적화 | 재귀를 더 안전하게 작성할 때 |
infix | 중위 표기 허용 | 함수 호출을 자연스럽게 표현할 때 |
operator | 연산자 오버로딩 | +, [] 같은 연산자 정의 시 |
external | 외부 네이티브 구현 사용 | JNI, 네이티브 코드 연결 시 |
inline | 함수 본문 인라인 | 고차 함수 오버헤드 감소가 필요할 때 |
noinline | 인라인 제외 람다 | inline 함수 안에서 특정 람다만 제외할 때 |
crossinline | 비지역 return 금지 | inline 람다의 return을 제한할 때 |
vararg | 가변 인자 | 인자 개수가 일정하지 않을 때 |
get, set | 접근자 커스터마이징 | 프로퍼티 읽기/쓰기 로직을 직접 제어할 때 |
lateinit은 “지금은 초기화하지 않지만, 나중에 반드시 값을 넣겠다”는 의미입니다.
class Service {
lateinit var config: String
}
lateinit은var에만 사용할 수 있으며,
nullable로 만들고 싶지 않지만 즉시 초기화가 어려운 경우에 유용합니다.
const는 컴파일 시점에 값이 확정되는 상수입니다.
const val PI = 3.14159
const는val과 비슷하지만,
top-level이나object,companion object안에서만 사용할 수 있습니다.
suspend는 코루틴 안에서 실행할 수 있는 함수입니다.
suspend fun fetchData(): String {
return "Data"
}
suspend는 “비동기 함수”라기보다
중단 가능한 함수라고 이해하는 편이 더 정확합니다.
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
fun main() {
val result = 3 add 5
println(result) // 8
}
a to b같은 문법도 이런 방식으로 동작합니다.
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은 함수 본문이 Kotlin에 없고, 외부 네이티브 코드에 구현되어 있음을 나타냅니다.
external fun nativeMethod(): Int
주로 JNI처럼 C/C++ 네이티브 코드와 연결할 때 사용합니다.
Java 메서드 호출을 의미하는 것은 아니라는 점을 구분해두면 좋습니다.
이 세 가지는 고차 함수에서 자주 등장합니다.
inline : 함수 본문을 호출 위치에 삽입noinline : 특정 람다는 인라인하지 않음crossinline : 비지역 return을 막음inline fun runAction(action: () -> Unit) {
action()
}
이 부분은 람다와 고차 함수를 어느 정도 익힌 뒤 보면 더 이해가 잘 됩니다.
나중에 따로 다뤄도 좋은 주제입니다.
vararg는 개수가 정해지지 않은 인자를 받을 수 있게 해줍니다.
fun sum(vararg numbers: Int): Int = numbers.sum()
fun main() {
val total = sum(1, 2, 3, 4)
println(total) // 10
}
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를 의미합니다.
| Modifier | 의미 | 언제 사용할까? |
|---|---|---|
object | 싱글턴 객체 선언 | 인스턴스가 하나만 필요할 때 |
companion object | 클래스 내부의 정적 멤버 비슷한 객체 | Java의 static처럼 사용하고 싶을 때 |
object는 싱글턴 객체를 만들 때 사용합니다.
object Singleton {
fun greet() = println("Hello")
}
프로그램 전체에서 하나만 존재해야 하는 객체를 간단하게 만들 수 있습니다.
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, internalopen, abstract, sealed, data, enum, annotation, inneroverride, lateinit, const, suspend, tailrec, infix, operator, external, inline, vararg, get, setobject, companion object기본기가 탄탄해야 코드를 더 의도적으로 작성할 수 있습니다.
수정자도 하나씩 익혀두면, 나중에는 문법이 아니라 코드의 설계 의도를 표현하는 도구로 보이기 시작합니다.