- 유용한 기능
- 확장함수
- 비동기 프로그래밍
- 쓰레드
- 코루틴
- 쓰레드와 코루틴
유용한 기능
자료형을 변환할 수 있다
일반 자료형 간의 변환 예시
var num1 = 20
var num2 = 30.2
var num3 = num2.toInt()
var num4 = num1.toDouble()
var strNum5 = "10"
var strNum6 = "10.21"
var num5 = Integer.parseInt(strNum5)
var num6 = strNum6.toDouble()
println("num3: $num3")
println("num4: $num4")
println("num5: $num5")
println("num6: $num6")
// 출력
num3: 30
num4: 20.0
num5: 10
num6: 10.21
- 숫자 자료형끼리는 to자료형() 메서드를 활용할 수 있다
- 문자열을 숫자로 변경할 때에는 별도의 메서드가 필요하다
객체 자료형 간의 변환 예시
- 객체 자료형 간의 변환은 상속관계에서 가능하다
업 캐스팅(자식 클래스를 부모 클래스의 자료형으로 객체 생성)
fun main() {
println("몇 마리를 생성하시겠습니까?")
var count = readLine()!!.toInt()
var birds = mutableListOf<Bird>()
for(idx in 0..count-1) {
println("조류의 이름을 입력해주세요")
var name = readLine()!!
// as Bird는 생략가능
birds.add(Sparrow(name) as Bird)
}
println("============조류 생성완료============")
for(bird in birds) {
bird.fly()
}
}
open class Bird(name: String) {
var name: String
init {
this.name = name
}
fun fly() {
println("${name}이름의 조류가 날아요~")
}
}
class Sparrow(name: String): Bird(name) {
}
// 출력
몇 마리를 생성하시겠습니까?
2 (입력 가정)
조류의 이름을 입력해주세요
참새 (입력 가정)
조류의 이름을 입력해주세요
비둘기 (입력 가정)
============조류 생성완료============
참새이름의 조류가 날아요~
비둘기이름의 조류가 날아요~
다운 캐스팅(부모클래스를 자식클래스의 자료형으로 객체 생성)
fun main() {
println("몇 마리를 생성하시겠습니까?")
var count = readLine()!!.toInt()
var birds = mutableListOf<Bird>()
for(idx in 0..count-1) {
println("조류의 이름을 입력해주세요")
var name = readLine()!!
birds.add(Sparrow(name) as Bird)
}
println("============조류 생성완료============")
for(bird in birds) {
bird.fly()
}
// 다운캐스팅 오류
// Sparrow는 Bird가 가져야할 정보를 모두 가지고 있지 않기 때문임
// var s1:Sparrow = birds.get(0)
}
open class Bird(name: String) {
var name: String
init {
this.name = name
}
fun fly() {
println("${name}이름의 조류가 날아요~")
}
}
class Sparrow(name: String): Bird(name) {
}
// 출력
몇 마리를 생성하시겠습니까?
2 (입력 가정)
조류의 이름을 입력해주세요
참새 (입력 가정)
조류의 이름을 입력해주세요
비둘기 (입력 가정)
============조류 생성완료============
참새이름의 조류가 날아요~
비둘기이름의 조류가 날아요~
자료형의 타입을 확인할 수 있다
if(name is String) {
println("name은 String 타입입니다")
} else {
println("name은 String 타입이 아닙니다")
}
- => 코틀린은 is 키워드를 활용해서 자료형의 타입을 확인할 수 있다
여러 인스턴스를 리턴할 수 있다
- 메서드는 기본적으로 하나의 데이터를 리턴한다
- 두 개 이상의 데이터를 포함하는 데이터클래스를 설계하고 인스턴스를 리턴하면 가능하다
- 하지만 매번 불필요한 클래스를 만드는 행위는 비효율적이다
- Pair를 활용하면 두 개의 값을 리턴할 수 있다(Triple을 사용하면 세 개의 인스턴스 리턴가능)
var chicken = Chicken()
var eggs = chicken.getEggs()
var listEggs = eggs.toList()
// first, second로 관리
// var firstEgg = eggs.first
// var secondEgg = eggs.second
// 리스트로 관리
var firstEgg = listEggs[0]
var secondEgg = listEggs[1]
println("달걀의 종류는 ${eggs} 입니다.")
println("리스트 달걀의 종류는 ${listEggs} 입니다.")
println("첫번째 달걀의 종류는 ${firstEgg} 입니다.")
println("두번째 달걀의 종류는 ${secondEgg} 입니다.")
}
class Chicken {
fun getEggs(): Pair<String, String> {
var eggs = Pair("달걀", "맥반석")
return eggs
}
}
// 출력
달걀의 종류는 (달걀, 맥반석) 입니다.
리스트 달걀의 종류는 [달걀, 맥반석] 입니다.
첫번째 달걀의 종류는 달걀 입니다.
두번째 달걀의 종류는 맥반석 입니다.
★ 자기 자신의 객체를 전달해서 효율적인 처리를 할 수 있다 ★
- 코틀린에서는 Scope Functions들을 제공한다
- 객체를 사용할 때 임시로 Scope를 만들어서 편리한 코드작성을 도와준다
코틀린의 Scope Functions 종류 예시
■ let function의 활용
: 중괄호 블록 안에 it으로 자신의 객체를 전달하고 수행된 결과를 반환한다
var strNum = "10"
var result = strNum?.let {
// 중괄호 안에서는 it으로 활용함
Integer.parseInt(it)
}
println(result!!+1)
// 출력
11
■ with function의 활용
: 중괄호 블록 안에 this로 자신의 객체를 전달하고 코드를 수행한다
: this는 생략해서 사용할 수 있으므로 반드시 null이 아닐 때만 사용하는 것이 좋다
var alphabets = "abcd"
with(alphabets) {
// var result = this.subSequence(0,2)
var result = subSequence(0,2)
println(result)
}
// 출력
ab
■ also function의 활용
: 중괄호 블록 안에 it으로 자신의 객체를 전달하고 객체를 반환해 준다
: apply와 함께 자주 사용한다
fun main() {
var student = Student("참새", 10)
var result = student?.also {
it.age = 50
}
result?.displayInfo()
student.displayInfo()
}
class Student(name: String, age: Int) {
var name: String
var age: Int
init {
this.name = name
this.age = age
}
fun displayInfo() {
println("이름은 ${name} 입니다")
println("나이는 ${age} 입니다")
}
}
// 출력
이름은 참새 입니다
나이는 50 입니다
이름은 참새 입니다
나이는 50 입니다
■ apply function의 활용
: 중괄호 블록 안에 this로 자신의 객체를 전달하고 객체를 반환해 준다
: 주로 객체의 상태를 변화시키고 바로 저장하고 싶을 때 사용한다
fun main() {
var student = Student("참새", 10)
var result = student?.apply {
student.age = 50
}
result?.displayInfo()
student.displayInfo()
}
class Student(name: String, age: Int) {
var name: String
var age: Int
init {
this.name = name
this.age = age
}
fun displayInfo() {
println("이름은 ${name} 입니다")
println("나이는 ${age} 입니다")
}
}
// 출력
이름은 참새 입니다
나이는 50 입니다
이름은 참새 입니다
나이는 50 입니다
■ run function의 활용
: 객체에서 호출하지 않는 경우의 예시
var totalPrice = run {
var computer = 10000
var mouse = 5000
computer+mouse
}
println("총 가격은 ${totalPrice}입니다")
// 출력
총 가격은 15000입니다
: 객체에서 호출하는 경우의 예시
-> with와 달리 null 체크를 수행할 수 있으므로 더욱 안전하게 사용이 가능하다
fun main() {
var student = Student("참새", 10)
student?.run {
displayInfo()
}
}
class Student(name: String, age: Int) {
var name: String
var age: Int
init {
this.name = name
this.age = age
}
fun displayInfo() {
println("이름은 ${name} 입니다")
println("나이는 ${age} 입니다")
}
}
// 출력
이름은 참새 입니다
나이는 10 입니다
Scope Functions에는 수신객체와 람다함수 간의 긴밀한 관계가 존재한다
- T는 수신객체를 의미한다
- block: 내부는 람다함수의 소스코드
- Scope Functions은 크게 두 가지로 구분할 수 있다
- 명시적으로 수신객체 자체를 람다로 전달하는 방법
- 수신객체를 람다의 파라미터로 전달하는 방법
- 수신객체는 it으로 사용할 수 있다
- 하지만 it은 Scope Function을 다중으로 사용할 때 문제가 발생할 수 있다
-> Child Function에서 Shadow가 되어서 제대로 참조하지 못할 수도 있다
- 그래서 it은 다른 이름으로 변경해서 사용하기도 한다
// Scope Function을 중첩으로 사용할 경우 누가 누구의 범위인지 알수 없음
// Implicit parameter 'it' of enclosing lambda is shadowed 경고 발생
data class Person(
var name: String = "",
var age: Int? = null,
var child: Person? = null
)
// 잘못된 예시
Person().also {
it.name = "한석봉"
it.age = 40
val child = Person().also {
it.name = "홍길동" // 누구의 it인지 모름
it.age = 10 // 누구의 it인지 모름
}
it.child = child
}
// 수정한 예시
Person().also {
it.name = "한석봉"
it.age = 40
val child = Person().also { c ->
c.name = "홍길동"
c.age = 10
}
it.child = child
}
let, also, apply, run, with를 표로 정리하면..
Scope에서 접근방식 this | Scope에서 접근방식 it | |
블록 수행 결과를 반환 | run, with | let |
객체 자신을 반환 | apply | also |
확장함수
기존 클래스에 쉽게 메서드를 추가할 수 있다
- 코틀린에서는 자바와 달리 외부에서 클래스의 메서드를 추가할 수 있다
- 과도하게 사용하면 코드의 가독성을 해줄 수 있지만 장점도 존재한다
- 원하는 메서드가 있지만 내가 설계한 클래스가 아닐 때 외부에서 메서드를 관리한다
- 내 목적을 위해 외부에서 관리하기 때문에 원본 클래스의 일관성을 유지할 수 있다
예시) Student 클래스를 변경하지 못하는 상황에서, Student 클래스 안에는 이름, 나이만 출력하는 displayInfo 함수가 있는데 추가로 등급까지 조회하고 싶다면?
fun main() {
fun Student.getGrade() = println("학생의 등급은 ${this.grade} 입니다")
var student = Student("참새", 10, "A+")
student.displayInfo()
student.getGrade()
}
class Student(name: String, age: Int, grade: String) {
var name: String
var age: Int
var grade: String
init {
this.name = name
this.age = age
this.grade = grade
}
fun displayInfo() {
println("이름은 ${name} 입니다")
println("나이는 ${age} 입니다")
}
}
// 출력
이름은 참새 입니다
나이는 10 입니다
학생의 등급은 A+ 입니다
비동기 프로그래밍
- 여러 가지 로직들이 완료 여부에 관계없이 실행되는 방식을 의미한다
- 순서대로 하나씩 작업을 수행하는 행위를 동기적 프로그래밍이라고 한다
- 만약 앞선 작업이 끝나지 않는다면 뒷작업은 영원히 수행할 수 없다
- 꼭 동기적으로 실행하지 않아도 되는 기능은 비동기적으로 실행하는 것이 좋다
동기 프로그래밍 : 요청을 보내고 결괏값을 받을 때까지 다른 작업을 멈춘다
비동기 프로그래밍 : 요청을 보내고 결괏값을 받을 때까지 멈추지 않고 또 다른 작업을 수행할 수 있다
-> 즉 동기는 한 가지씩 작업을 처리하고 비동기는 다양한 일을 한꺼번에 수행한다
쓰레드
로직을 동시에 실행할 수 있도록 도와준다
- 프로그램은 하나의 메인 쓰레드(실행흐름)가 존재한다
- 하나의 메인 쓰레드는 ㅡ -> fun main() <- ㅡ 메인함수를 의미한다
- 실습 프로그램은 메인 쓰레드 위에서 로직을 실행해서 동시처리가 불가능했다
- 별도의 자식 쓰레드를 생성해서 동시에 로직을 실행할 수 있다
- 코틀린은 thread 키워드로 쓰레드를 생성할 수 있다
프로세스(Process)
- 프로그램이 메모리에 올라가서 실행될 때, 이를 프로세스 1개라고 한다
- 보통 프로그램을 더블클릭하면 프로세스가 생긴다
쓰레드(Thread)
- 쓰레드는 프로세스보다 더 작은 단위이다
- 프로세스 안에서 더 작은 작업의 단위를 쓰레드라고 한다
- 쓰레드는 생성돼서 수행할 때, 각 독립된 메모리 영역인 STACK을 가진다
- 즉, 쓰레드를 한 개 생성하면 스택메모리의 일정 영역을 차지한다
쓰레드를 사용하는 이유?
- 몬스터를 공격하고, 체력이 줄어들고, 효과음이 동시에 발생해야 한다
- 경마 프로그램의 말들은 동시에 출발해서 경쟁해야 한다
쓰레드를 사용하려면..
외부 종속성을 추가해줘야 한다
build.gradle.kts에 밑의 코드를 입력해 주고 빨간 줄 뜨는 것들 임포트 시킨다
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
코루틴
운영체제의 깊이 있는 지식이 없어도 쉽게 비동기 프로그래밍을 할 수 있다
- 최적화된 비동기 함수를 사용한다
- 하드웨어 자원의 효율적인 할당을 가능하게 한다
- 안정적인 동시성, 비동기 프로그래밍을 가능하게 한다
- 코루틴이 쓰레드보다 더욱 가볍게 사용할 수 있다
- 로직들을 협동해서 실행하자는 게 목표이며 구글에서 적극 권장한다
코루틴은 빌더와 함께 사용한다
- 일반적으로 launch와 async 빌더를 가장 많이 사용한다
- launch는 결괏값이 없는 코루틴 빌더를 의미한다
- launch는 Job객체로 코루틴을 관리한다
- async는 결괏값이 있는 코루틴이고 Deffered 타입으로 값을 리턴해준다
- 코루틴은 스코프로 범위를 지정할 수 있다
-> GlobalScope : 앱이 실행된 이후에 계속 수행되어야 할 때 사용한다}
-> CoroutinScope : 필요할 때만 생성하고 사용 후에 정리가 필요하다 - 코루틴을 실행할 쓰레드를 Dispatcher로 지정할 수 있다(쓰레드를 정해야 코루틴을 쓸 수 있다)
-> Dispatchers.Main : UI와 상호작용하기 위한 메인쓰레드
-> Dispatchers.IO : 네트워크나 디스크 I/O 작업에 최적화되어 있는 쓰레드
-> Dispatchers.Default : 기본적으로 CPU 최적화되어 있는 쓰레드
예시)
- 안드로이드는 항상 앱이 켜져 있는 상태지만 실습환경은 실행 후 종료되는 JVM환경이다
- 따라서 실행하면 main이 먼저 종료되기 때문에 코루틴의 결과를 얻을 수 없다
fun main(args: Array<String>) {
println("메인쓰레드 시작")
var job = GlobalScope.launch {
delay(3000) // 3초 딜레이
println("여기는 코루틴...")
}
println("메인쓰레드 종료")
}
// 출력
메인쓰레드 시작
메인쓰레드 종료
- 비동기적으로 코루틴의 결과를 조회하고 싶다면 job의 join메서드를 활용할 수 있다
- 안드로이드와 달리 앱이 항상 켜져있지 않기 때문에 job으로 실습을 진행한다
fun main(args: Array<String>) {
println("메인쓰레드 시작")
var job = GlobalScope.launch {
delay(3000)
println("여기는 코루틴...")
}
runBlocking {
// job을 이용해서 코루틴이 끝날때까지 기다리게 한다
job.join()
}
println("메인쓰레드 종료")
}
// 출력
메인쓰레드 시작
여기는 코루틴... - (3초딜레이 후 출력)
메인쓰레드 종료
- 결괏값을 리턴 받아야 하기 때문에 await은 일시중단이 가능한 코루틴에서 실행가능하다
println("메인쓰레드 시작")
var job = CoroutineScope(Dispatchers.Default).launch {
var fileDownloadCoroutine = async(Dispatchers.IO) {
delay(10000) // 10 초 딜레이
"파일 다운로드 완료"
}
var databaseConnectCoroutine = async(Dispatchers.IO) {
delay(5000) // 5초 딜레이
"데이터베이스 연결 완료"
}
println("${fileDownloadCoroutine.await()}")
println("${databaseConnectCoroutine.await()}")
}
runBlocking {
job.join()
}
println("메인쓰레드 종료")
job.cancel()
// 출력
메인쓰레드 시작
파일 다운로드 완료 - (10초 딜레이 후 출력)
데이터베이스 연결 완료 - (5초 딜레이 후 출력)
메인쓰레드 종료
쓰레드와 코루틴
동시성 프로그래밍
- 쓰레드와 코루틴은 둘 다 동시성 프로그래밍을 위한 기술
- 동시성 프로그래밍 기술은 컨텍스트 스위칭이 중요한 개념이다
쓰레드와 코루틴의 차이점
쓰레드
- 작업 하나하나의 단위 : Thread
- 각 Thread가 독립적인 Stack 메모리 영역을 가진다 - 동시성 보장 수단 : Context Switching
- 운영체제 커널에 의한 Context Switching을 통해서 동시성을 보장한다고 한다
블로킹
-> Thread A가 Thread B의 결과를 기다린다
-> 이때, Thread A는 블로킹 상태라고 할 수 있다
-> A는 Thread B의 결과가 나올 때까지 해당 자원을 사용하지 못한다
예시)
- Thread A가 Task 1을 수행하는 동안 Task 2의 결과가 필요하면 Thread B를 호출
- 이때, Thread A는 블로킹되고 Thread B로 프로세스 간의 스위칭이 일어나 Task 2를 수행한다
- Task 2가 완료되면 Thread A로 다시 스위칭해서 결괏값을 Task 1에게 반환한다
- 이때, Task 3, Task 4는 A, B작업이 진행되는 도중에 멈추지 않고 각각 동시에 실행한다
- 이때, 컴퓨터 운영체제 입장에서는 각 Task를 쪼개서 얼마나 수행할지가 중요하다
- 그래서 어떤 쓰레드를 먼저 실행해야 할지 결정하는 행위를 스케쥴링이라고 한다
- 이러한 행위를 통해 동시성을 보장한다
코루틴
- 작업 하나하나의 단위: Coroutine Object
- 여러 작업 각각에 Object를 할당한다
- Coroutine Object도 엄연한 객체이기 때문에 JVM Heap에 적재한다(코틀린 기준) - 동시성 보장 수단: Programmer Switching (No-Context Switching)
- 소스 코드를 통해 Switching 시점을 마음대로 정한다 (OS는 관여하지 않는다)
Suspend(Non-Blocking)
-> Object 1이 Object 2의 결과를 기다릴 때 Object 1의 상태는 Suspend로 바뀐다
-> 그래도 Object 1을 수행하던 Thread는 그대로 유효.
-> 그래서 Object 2도 Object 1과 동일한 Thread에서 실행된다
예시)
- Coroutine은 작업 단위가 Object이다
- Task 1을 수행하다가 Task 2의 수행요청이 발생했다면
- Context Switching 없이 동일한 Thread A에서 수행할 수 있다
- Thread C처럼 하나의 쓰레드에서 여러 Task Object들을 동시에 수행할 수 있다
- 이러한 특징 때문에 코루틴을 Light-Weight Thread라고 한다
- 동시처리를 위해 스택영역을 별도로 할당하는 쓰레드처럼 동작하지 않는다
- 그러면서도 동시성을 보장할 수 있다
- 하나의 쓰레드에서 다수의 코루틴을 수행할 수 있다
- 커널의 스케쥴링을 따르는 Context Switching을 수행하지 않는다
요약
- 쓰레드나 코루틴은 각자의 방법으로 동시성을 보장하는 기술이다
- 코루틴은 Thread를 대체하는 기술은 아니다 (엄연히 다른 기술)
- 코루틴은 하나의 Thread를 더욱 잘게 쪼개서 사용하는 기술이다
- 코루틴은 쓰레드보다 CPU 자원을 절약하기 때문에 Light-Weight Thread라고 한다
- 구글에서는 코틀린의 코루틴 사용을 적극 권장한다
'코틀린(Kotlin) > TIL' 카테고리의 다른 글
[코틀린(Kotlin)] Intent, findViewById (0) | 2023.12.11 |
---|---|
[깃허브(GitHub)] 깃허브를 활용하는 협업(Branch, Pull Requests) (0) | 2023.12.04 |
[Kotlin 문법] 객체지향 프로그래밍의 심화 (0) | 2023.11.30 |
[Kotlin 문법] 객체지향 프로그래밍의 기초 (0) | 2023.11.28 |
[Kotlin 문법] 기초 - 단축키 및 기초지식 (1) | 2023.11.27 |