1. object와 companion object
1-1. object
코틀린에서 "object" 키워드는, 주로 싱글톤(Singleton) 객체를 생성하는 데 사용된다.
생성자는 사용 불가하고, 프로퍼티, 메서드, 초기화 블록은 사용 가능하다.
다른 클래스나 / 인터페이스를 상속받을 수 있다.
➡️ object의 3가지 주요 용도
1) 싱글톤 객체 생성
- object 키워드를 사용하면 싱글톤 객체를 쉽게 만들 수 있다.
- 해당 object는 전역 변수처럼 사용할 수 있으며, 한 번만 생성된다.
- 다른 클래스에서 `ClassName.objectName` 형태로 접근할 수 있다.
object Logger {
fun log(message: String) {
println("LOG: $message")
}
}
// 다른 클래스에서 사용
Logger.log("Hello, World!")
2) 익명 객체 생성
- object 키워드를 사용하면 익명 객체를 쉽게 만들 수 있다.
- 인터페이스 구현 시 유용하게 사용할 수 있다.
val clickListener = object : View.OnClickListener {
override fun onClick(v: View) {
// 클릭 처리 로직
}
}
3) 동반 객체(Companion Object)
- 클래스 내부에 "companion object" 블록을 정의하면 클래스 레벨의 메소드와 프로퍼티를 구현할 수 있다.
- 자바의 정적 메소드 및 필드와 유사한 기능을 제공한다.
※ 1장에서 자세히 설명
object 키워드를 통해 코틀린 개발자는 보다 간결하고 명시적인 방식으로 객체 지향 프로그래밍을 할 수 있다.
1-2. companion object
자바의 정적 메소드와 필드(static method and fields)와 유사한 기능을 제공한다.
즉, companion object는 클래스에 연관된 메소드와 프로퍼티를 정의할 수 있게 해주는 기능이다.
companion object는 클래스 당 하나만 생성할 수 있다.
➡️ companion object의 4가지 특징
1. 클래스 레벨의 메소드와 프로퍼티 : companion object는 클래스 레벨에서 메소드와 프로퍼티를 정의할 수 있다.
이를 통해 인스턴스를 생성하지 않고도 클래스 관련 기능을 사용할 수 있다.
2. 싱글톤 패턴 : companion object는 싱글톤 패턴을 구현하는 데 사용할 수 있다.
'하나의 companion object 인스턴스가 전역적으로 접근 가능하기 때문에 싱글톤 패턴을 구현하기 좋다.
3. 이름 지정 가능 : companion object에는 이름을 지정할 수 있다.
이름을 지정하면 "ClassName.companionName.methodName()" 과 같은 형태로 접근할 수 있다.
4. 확장 함수 사용 가능 : companion object에 정의된 메소드와 프로퍼티는 클래스의 확장 함수로 정의할 수 있다.
class MyClass {
companion object Factory {
fun create(): MyClass {
return MyClass()
}
val DEFAULT_VALUE = 42
}
fun doSomething() {
println("Doing something...")
}
}
fun main() {
// companion object에 정의된 메소드와 프로퍼티 사용
val instance = MyClass.create()
instance.doSomething()
println(MyClass.DEFAULT_VALUE)
// 확장 함수로 정의된 메소드 사용
MyClass.myExtensionMethod()
}
fun MyClass.Companion.myExtensionMethod() {
println("This is an extension function of the companion object.")
}
위의 예제에서 MyClass의 companion object에는 "create()" 메소드와 "DEFAULT_VALUE" 프로퍼티가 정의되어 있다.
이를 통해 MyClass의 인스턴스를 생성하거나, DEFAULT_VALUE에 접근할 수 있다.
또한 "myExtensionMethod()" 는 companion object에 대한 확장 함수로 정의되어 있다.
이처럼 companion object는 클래스 레벨의 기능을 제공하여 코드의 재사용성과 가독성을 높일 수 있다.
2. null 처리
2-1. !! 연산자
코틀린에서 !! 연산자는 '강제 언랩' 또는 '안전하지 않은 언랩'으로 불리며, null이 아닌 값으로 변환하는 역할을 한다.
➡️ !! 연산자의 2가지 주요 특징
1. null 가능성이 있는 값 언랩하기
코틀린에서는 기본적으로 모든 참조형 변수가 null을 가질 수 있다.
이때 !! 연산자를 사용하면 null 가능성이 있는 값을 null이 아닌 값으로 변환할 수 있다.
var str: String? = "Hello"
println(str!!.length) // 5
str = null
println(str!!.length) // NullPointerException 발생
2. Nullable 변수를 사용할 때
변수가 null이 아닌 것을 확신한다면 -> !! 연산자를 사용하여, null이 아님을 확실히 할 수 있다.
val length = str?.length ?: 0 // str이 null이면 0을 반환
val actualLength = str!!.length // str이 null이면 NullPointerException 발생
하지만 !! 연산자 사용 시 주의해야 할 점이 있다 :
1) !! 연산자를 사용했는데, 값이 null이라면 -> "NullPointerException" 이 발생할 수 있다.
따라서 값이 null일 수 있는 경우에는 !! 연산자 대신, 안전 호출 연산자(?.)나 엘비스 연산자(?:)를 사용하는 것이 좋다.
2) !! 연산자는 코드의 안전성을 저하시킬 수 있으므로, 반드시 필요한 경우에만 사용해야 한다.
2-2. 안전 호출 연산자 ?.
null 체크 후, 안전하게 프로퍼티나 메서드에 접근할 수 있다.
2-3. 엘비스 연산자 ?:
null인 경우, 대체 값을 반환할 수 있다.
※ 또는 let, run (객체 초기화), apply 등의 함수 : null 체크 후, 안전하게 코드를 실행할 수 있다.
4. 코틀린의 Scope function 5가지
1) let 함수
코틀린의 let 함수는 it (생략 불가능)을 수신 객체로 사용하고, 확장함수로 호출한다.
그리고 "람다 식의 마지막 행" 을 반환한다.
➡️ let의 4가지 주요 특징
1) null 안전성 높이기
- let 함수는 null 값인 경우, 코드 블록을 실행하지 않는다.
- 이를 통해 NullPointerException 을 방지할 수 있다.
val str: String? = "Hello, World!"
str?.let {
println(it.length) // 'it'은 str 변수의 값을 가리킴
}
2) 변수 범위 제한하기
- let 함수 내부에서 사용되는 변수는, let 블록 내부로 범위가 제한된다.
- 이를 통해 불필요한 변수 선언을 줄일 수 있다.
val length = str?.let {
val tmpLength = it.length
println("Length: $tmpLength")
tmpLength
} ?: 0
3) 코드 흐름 제어
- let 함수 내부에서 반환값을 지정하면, 해당 값이 let 함수의 반환값이 된다.
- 이를 통해 복잡한 코드 흐름을 간단하게 표현할 수 있다.
val result = str?.let {
if (it.length > 5) {
"Long string"
} else {
"Short string"
}
} ?: "Null string"
4) 함수형 프로그래밍 스타일 지원
- let 함수는 함수형 프로그래밍 스타일의 코드 작성을 지원한다.
- 람다식과 함께 사용하면, 가독성 높은 코드를 작성할 수 있다.
val list = listOf(1, 2, 3, 4, 5)
list.filter { it > 2 }
.map { it * it }
.let { println(it) } // [9, 16, 25]
2) apply 함수
코틀린의 apply 함수는 수신 객체(receiver) 자신이 반환되기 때문에, 객체 초기화와 구성에 유용하게 사용된다.
또한 수신 객체를 this(생략 가능)로 사용하고, 확장함수로 호출한다.
➡️ apply의 4가지 주요 특징
1) 객체 구성 및 초기화
- 객체의 프로퍼티를 설정하는 등의 작업을 수행할 수 있다.
- 이를 통해 객체 생성 및 초기화 코드를 간단하게 작성할 수 있다.
val person = Person("John Doe", 30).apply {
address = "123 Main St"
phoneNumber = "555-1234"
}
2) 연쇄 호출 지원
- apply 함수는 객체 자신을 반환하므로, 메서드 체이닝(method chaining)을 지원한다.
- 이를 통해 가독성 높은 코드를 작성할 수 있다.
val textView = TextView(context).apply {
text = "Hello, World!"
textSize = 16f
setOnClickListener { /* 클릭 처리 */ }
}
3) null 안전성 향상
- apply 함수는 null 값을 허용하므로, null 안전성을 높일 수 있다.
- let 함수와 함께 사용하면, null 처리 로직을 간단하게 작성할 수 있다.
val str: String? = "Hello"
str?.let { println(it) }
?.apply { println("Length: $length") }
4) 구현 로직 숨기기
- apply 함수를 사용하면, 객체 구성 및 초기화 로직을 메서드 호출 내부에 숨길 수 있다.
- 이를 통해 코드의 가독성을 높일 수 있다.
fun createTextView(context: Context): TextView {
return TextView(context).apply {
text = "Hello, World!"
textSize = 16f
setOnClickListener { /* 클릭 처리 */ }
}
}
3) run 함수
코틀린의 run 함수는 this(생략 가능)를 수신 객체로 사용하고, 확장함수로 호출한다.
그리고 "람다 식의 마지막 행" 을 반환한다.
4) also 함수
코틀린의 also 함수는 it (생략 불가능)을 수신 객체로 사용하고, 확장함수로 호출한다.
그리고 "수신객체" 를 반환한다.
5) with 함수
코틀린의 with 함수는 this(생략 가능)를 수신 객체로 사용하고, with 함수의 인자 형태로 호출한다.
그리고 "람다 식의 마지막 행" 을 반환한다.
5. 지연 초기화 (Lazy Initialization)
지연 초기화 : 변수가 실제로 필요할 때까지 초기화를 미루는 기능
5-1. val과 by lazy
코틀린에서 val(불변 객체) 뒤에 "by lazy {}" 구문을 붙이면, 지연 초기화 기능을 사용하는 것이다.
➡️ val + by lazy의 3가지 주요 특징
1) 지연 초기화를 통한 메모리 절약
- 변수가 실제로 사용되기 전까지는 초기화를 하지 않음으로써, 불필요한 메모리 사용을 줄일 수 있다.
- 애플리케이션 실행 시간이 오래 걸리는 복잡한 초기화 작업을 미룰 수 있다.
2) Singleton 패턴 구현
- 지연 초기화를 사용하면, Singleton 패턴을 간단하게 구현할 수 있다.
- 처음 사용될 때 단 한 번만 객체가 생성되도록 보장할 수 있다.
3) 불변성 유지
- 지연 초기화를 사용하면, 변수의 불변성을 쉽게 유지할 수 있다.
- 초기화 시점을 지연시켜, 변수에 대한 접근을 통제할 수 있다.
class MyClass {
// 지연 초기화된 프로퍼티
val lazyProperty: String by lazy {
println("Initializing lazyProperty")
"Hello, World!"
}
fun uselazyProperty() {
println(lazyProperty) // 처음 호출될 때만 "Initializing lazyProperty" 출력
println(lazyProperty) // 두 번째 호출 시 캐싱된 값 반환
}
}
fun main() {
val myClass = MyClass()
myClass.uselazyProperty() // 처음 호출 시 초기화, 이후 호출 시 캐싱된 값 반환
}
위 예제에서 "lazyProperty"는 "by lazy { }" 블록을 통해 지연 초기화된다. "uselazyProperty()" 함수를 처음 호출하면 "Initializing lazyProperty" 메시지가 출력되고, 이후에는 캐싱된 값이 반환된다.
5-2. var과 lateinit
반대로 var(변수) 앞에 lateinit을 붙여서 선언해야, 지연 초기화를 할 수 있다.
이때, Primitive(Int 등등) 타입은 lateinit을 사용할 수 없다 -> Integer 같은 래퍼 타입으로 대체
6. [안드로이드 문법] lifecycleScope와 launch
코틀린과 안드로이드에서, "lifecycleScope" 와 "lifecycleScope.launch {}" 는 다음과 같은 역할을 한다.
1) lifecycleScope
- lifecycleScope는 안드로이드 컴포넌트(Activity, Fragment 등)의 생명주기와 연결된 CoroutineScope이다.
- 이 CoroutineScope는 컴포넌트의 생명주기와 연결되어, 컴포넌트가 소멸되면 자동으로 해당 CoroutineScope도 취소된다.
- 따라서 lifecycleScope 를 사용하면, 메모리 누수와 같은 문제를 방지할 수 있다.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
lifecycleScope.launch {
// 코루틴 작업 수행
doSomeWork()
}
}
private suspend fun doSomeWork() {
// 코루틴 작업 구현
}
}
2) lifecycleScope.launch {}
- lifecycleScope.launch {} 는, lifecycleScope의 launch 함수를 사용하여 코루틴을 실행하는 것이다.
- launch 함수는 CoroutineBuilder로, 새로운 코루틴을 시작하는 역할을 한다.
- lifecycleScope.launch {} 를 사용하면, 코루틴 작업을 컴포넌트의 생명주기와 연결할 수 있다.
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
// 코루틴 작업 수행
fetchData()
}
}
private suspend fun fetchData() {
// 데이터 가져오기 작업 구현
val data = getData()
// UI 업데이트 등의 작업 수행
}
}
이처럼 lifecycleScope 는 안드로이드 애플리케이션 개발에서 코루틴을 사용할 때 매우 유용한 기능이다.
컴포넌트의 생명주기에 맞춰 코루틴 작업을 실행하고 관리할 수 있어, 메모리 누수 등의 문제를 효과적으로 해결할 수 있다.
7. vararg
코틀린에서 "vararg"는 가변 인자 리스트(variable-length argument list)를 지정하는 키워드이다.
이를 통해 메소드나 생성자에 가변 개수의 인자를 전달할 수 있다.
➡️ vararg의 3가지 주요 특징
1) 가변 개수의 인자 받기
- vararg 매개변수를 사용하면, 메소드나 생성자에 가변 개수의 인자를 전달할 수 있다.
- 인자의 개수는, 0개부터 ~ 임의의 개수까지 가능하다.
fun sum(vararg numbers: Int): Int {
return numbers.sum()
}
// 사용 예시
println(sum(1, 2, 3)) // 출력: 6
println(sum()) // 출력: 0
2) 배열 전달하기
- vararg 매개변수에는 개별 인자뿐만 아니라, 배열도 전달할 수 있다.
- 배열을 vararg 매개변수에 전달할 때는, 스프레드 연산자(`*`)를 사용해야 한다.
- 스프레드 연산자는 배열의 각 요소를, 개별 인자로써 전달한다.
val numbers = intArrayOf(1, 2, 3)
println(sum(*numbers)) // 출력: 6
3) 가변 인자 뒤에 다른 인자 사용 불가!
- 따라서 vararg 매개변수는, 메소드나 생성자의 마지막 매개변수로 사용해야 한다.
- 그 뒤에 다른 매개변수를 추가할 수 없다.
fun printAll(prefix: String, vararg items: String) {
for (item in items) {
println("$prefix $item")
}
}
printAll("Hello", "John", "Jane", "Tom")
vararg 키워드를 사용하면, 코드의 재사용성과 유연성을 높일 수 있다.