공부가 필요한 문법에 대해서 정리한 글입니다.
코틀린의 클래스, 함수, 변수, 프로퍼티, 생성자 등에는 앞에 부과적인 의미를 부여하는 키워드가 존재합니다.
이번 포스트에서는 모든 수정자(Modifiers)를 정의해보려고 합니다.
코틀린 공식문서에 따르면 다음과 같은 수정자들이 존재합니다.
접근 범위 제어(public, private)부터 상속 여부(open, final), 가시성(internal) 등을 표현할 수 있는 다양한 키워드가 존재합니다.

접근제어자는 간단하게 말하면 코드의 접근 범위를 지정할 때 사용됩니다.
| 접근 제어자 | 사용 기능 위치 | 접근 범위 | 언제 사용하나? |
|---|---|---|---|
| public | 어디서든 | 모든 곳에서 접근 가능 | 외부 모듈/클래스에서도 사용할 수 있도록 할 때 |
| private | 클래스, 파일, 블록 내부 | 정의된 범위 내에서만 접근 가능 | 외부에 노출시키고 싶지 않은 내부 구현일 때 |
| protected | 클래스 내부 + 하위 클래스 | 외부에서는 접근 불가 | 상속 관계에서만 접근 가능하게 하고 싶을 때 |
| internal | 같은 모듈 내 | 같은 모듈에서만 접근 가능 | 모듈 내부에선 공유하지만, 외부에선 숨기고 싶을 때 |
//어디서든 사용 가능한 클래스 (선언 안해도 자동으로 public 클래스임.)
public class Animal(val name: String)
//secret는 Animal 클래스 외부에서는 접근 불가
class Animal {
private val secret = "숨겨진 값"
}
open class Animal {
protected fun sound() { println("울음 소리!") }
}
//상속받은 클래스 내부에서는 사용 가능
class Dog : Animal() {
fun walwal() {
//사용 가능
sound()
}
}
class Cat {
fun miaumiau() {
//사용 불가
sound()
}
}
internal fun doSomething() {}
fun main(){
//같은 모듈 내에서 사용 가능
doSomething()
}
클래스의 앞에서 사용됩니다. 용도에 따라 상속을 정의해주며 의미는 다음 표와 같습니다.
| Modifier | 정의 | 언제 사용하나? |
|---|---|---|
| open | 다른 클래스가 상속 가능하도록 허용 | 하위 클래스에서 재정의(override)할 수 있게 하고 싶을 때 |
| final | 상속 불가능 (기본값) | 기본 클래스에서 수정되면 안 되는 기능일 때 |
| abstract | 인스턴스를 만들 수 없고, 반드시 하위 클래스가 구현해야 함 | 템플릿용 클래스를 만들고 구체적인 구현을 위임하고 싶을 때 |
| sealed | 제한된 클래스 상속만 허용 (같은 파일 내에서만 상속 가능) | 조건 분기 시 타입을 제한하고 exhaustive when을 쓰고 싶을 때 |
| inner | 내부 클래스가 외부 클래스 참조 가능 | 중첩 클래스에서 외부 클래스의 필드나 메서드에 접근하고 싶을 때 |
| data | equals, hashCode, toString 등을 자동 생성하는 데이터 전용 클래스 | DTO, 값 객체, 단순 모델 클래스 만들 때 |
| enum | 열거형 클래스 | 상수값을 그룹화해서 표현하고 싶을 때 |
| annotation | 어노테이션 정의 | 커스텀 어노테이션을 정의할 때 |
//코틀린은 기본 final이라 open이 없으면 override가 불가합니다.
open class Animal {
open fun speak() = println("동물 소리")
}
class Cat : Animal() {
override fun speak() {
println("야옹!")
}
}
//추상 메서드는 하위 클래스에서 구현됩니다. 이를 사용하면 인스턴스를 생성할 수 없습니다.
//또한 추상 클래스는 설계도를 만들어 주면 자식이 알아서 책임진다는 특징이 있습니다.
abstract class Shape {
abstract fun area(): Double
}
class Circle(val r: Double) : Shape() {
override fun area() = Math.PI * r * r
}
// 같은 파일 내에서만 상속 가능합니다.
sealed class Result
class Success(val data: String) : Result()
class Error(val message: String) : Result()
//내부 클래스에서 외부 클래스 참조가 가능합니다.
class Outer {
val outerName = "Outer"
inner class Inner {
fun print() = println(outerName)
}
}
fun main(){
val n = Outer().Inner().print() //Outer
}
//자동으로 equals, hashCode, toString 메소드가 생성됩니다.
data class User(val id: Int, val name: String)
//멤버의 집합을 열거형 타입인 Enum으로 묶어줄 수 있습니다.
//독립된 특수한 클래스로 구분합니다.
enum class Direction(val desc: String) {
NORTH("북"), SOUTH("남"), EAST("동"), WEST("서")
}
//어노테이션은 메타데이터를 코드에 첨부하는 수단입니다.
//지금은 커스텀 어노테이션을 만드는 방법에 대한 코드입니다.
annotation class MyAnnotation
@MyAnnotation
fun main(){
}
| Modifier | 정의 | 언제 사용하나? |
|---|---|---|
| override | 부모 클래스의 멤버를 재정의 | 상속받은 함수를 커스터마이징하고 싶을 때 |
| lateinit | 나중에 초기화될 프로퍼티 (var 전용) | 의존성 주입, 초기화가 늦게 일어나는 프로퍼티에 사용 |
| const | 컴파일 타임 상수 (val + top-level, object 내부에서만 가능) | 변하지 않는 전역 상수를 정의하고 싶을 때 |
| suspend | 코루틴에서 실행 가능한 함수 | 비동기 처리, 네트워크/IO 등 중단 가능한 작업을 정의할 때 |
| tailrec | 꼬리 재귀 최적화 함수로 컴파일됨 | 재귀 호출이 깊어질 때 스택 오버플로우 방지하고 싶을 때 |
| infix | 중위 호출 허용 | a to b처럼 함수 호출을 자연스럽게 표현하고 싶을 때 |
| operator | 연산자 오버로딩 | +, [] 같은 연산자를 커스터마이징하고 싶을 때 |
| external | 외부(C/C++)에서 정의된 함수 | JNI, 네이티브 라이브러리를 연결할 때 |
| inline | 고차 함수를 인라인 처리 (성능 최적화) | 람다를 사용하는 함수에서 오버헤드를 줄이고 싶을 때 |
| noinline | inline 함수 내부에서 인라인하지 않을 인자에 사용 | 인라인은 안 하고 싶은 특정 람다 인자에 |
| crossinline | 인라인 함수 내 람다에서 return 금지 | 함수 바깥으로 빠져나가는 것을 방지하고 싶을 때 |
| vararg | 가변 인자 허용 | 개수 제한 없이 여러 인자를 받을 때 (fun sum(vararg nums: Int)) |
| get, set | 프로퍼티의 접근자 커스터마이징 | 커스텀 로직이 필요한 경우 (val x get() = ...) |
/**
변수를 선언할 때 반드시 초기화가 이뤄지게 되는데 lateinit을 사용하게 되면 언제 초기화가 이루어질지 모릅니다.
즉, 나중에 초기화가 된다는 소리이며 불필요한 nullable을 막아주는 역할을 한다. 또한 var로만 사용가능합니다.
val이 안되는 이유는 lateinit은 내부에서 null로 초기화 한 이후에
들어오는 값으로 변경하는 방식으로 구현되어 있기 때문입니다.
**/
class Service {
lateinit var config: String
}
// 컴파일 타임 상수이며 val과 달리 const는 top-level이나 object 내부에서만 사용가능합니다.
// 변하지 않는 전역 상수를 정의하고 싶을 때 사용합니다.
const val PI = 3.14159
// 코루틴에서 사용할 수 있는 비동기 함수입니다.
// 특징은 하나의 함수가 다 처리될때 까지 기다리는 것이 아닌 동시에 함수를 처리할 수 있다는 장점이 있습니다.
suspend fun fetchData(): String {
delay(1000)
return "Data"
}
// 재귀 호출이 깊어질 때 최적화하기 위해 사용됩니다.
tailrec fun factorial(n: Int, acc: Int = 1): Int =
if (n <= 1) acc else factorial(n - 1, n * acc)
//중위 표기법으로 함수를 더 자연스럽게 사용 가능하게 해줍니다.
infix fun Int.add(x: Int): Int = this + x
val result = 3 add 5 // 8
// +, -, [] 같은 연산자를 커스터마이징할 때 사용됩니다.
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
/**
함수나 클래스의 본문이 Kotlin 코드에 포함되지 않고 외부에서 제공되는 것임을 나타냅니다.
Kotlin에서 Java의 메서드나 클래스를 호출하거나
Java 코드에서 Kotlin의 메서드나 클래스를 호출할 때 사용할 수 있습니다.
**/
external fun nativeMethod(): Int
fun main() {
val result = nativeMethod()
println("Result from native method: $result")
}
/**
고차 함수를 최적화하는 키워드입니다.
기본적으로 고차 함수를 사용하면 런타임 패널티가 있기 때문에 함수 구현 자체를 코드에 넣음으로써
오버헤드를 없앨 수 있습니다.
이건 다음 포스트에 한 번 다루도록 하겠습니다.
**/
//여러 개의 인자를 배열처럼 받을 수 있게 해줍니다.
fun sum(vararg numbers: Int): Int = numbers.sum()
val total = sum(1, 2, 3, 4) // 10
/**
코틀린은 변수를 만들어주기만 해도 자동적으로 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 Singleton {
fun greet() = println("Hello")
}
/**
static 처럼 정적 멤버를 정의할 수 있습니다.
외부에서는 MyClass.VERSION 로 사용합니다.
**/
class MyClass {
companion object {
const val VERSION = "1.0"
fun create() = MyClass()
}
}
object와 companion을 사용한 싱글톤 패턴을 구현하는 법에 대해서도 나중 포스트로
자세하게 다뤄보겠습니다.
이번 포스트에서는 코틀린 공식문서에 있는 수정자 Modifier를 다뤄보았습니다.
기본기가 탄탄해야 코드를 작성할 때 더욱 효율적으로 작성하는 것 같습니다.
위의 키워드를 사용해 클린 코드에 가까워져 봅시다!