본문 바로가기

개발 노트/Kotlin

[kotlin] 문법 5주차 정리 - 심화

# 유용한 여러기능

 

# 자료형을 변환할 수 있다 

 

- 일반 자료형간의 변환 

  • 숫자 자료형끼리는 to자료형() 메소드를 활용할 수 있다
  • 문자열을 숫자로 변경할때에는 별도의 메소드가 필요하다 (Integer.parseInt 사용)
    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

 

 

- 객체 자료형간의 변환 

  • 객체 자료형간의 변환상속관계에서 가능하다

 

  • 업 캐스팅 예제 코드 (자식클래스를 부모클래스의 자료형으로 객체 생성)
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
조류의 이름을 입력해주세요
독수리
조류의 이름을 입력해주세요
참새
============조류 생성완료============
독수리이름의 조류가 날아요~
참새이름의 조류가 날아요~

 

-> birds라는거는 Bird클래스만 들어갈수있도록 제한을 해놨는데, birds.add에서 Sparrow를 바로 넣을 수 없는데 as Bird를 사용해서 Bird클래스로써 들어갈 수 있게끔 업케스팅을 해놨기때문에 들어갈 수 있는것이다 

-> 원래는 독수리나 참새 객체를 따로따로 만들어서 fly를 했는데, 지금은 Bird안에다가 바로 리스트를 만들어서 업케스팅을 통해 객체를 만들고 한번에 bird.fly()를해서 호출을 했다. (한번에 호출이 가능한게 업케스팅 때문)

 

 

  • 다운 캐스팅 예제코드(부모클래스를 자식클래스의 자료형으로 객체 생성
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
조류의 이름을 입력해주세요
독수리
조류의 이름을 입력해주세요
참새
============조류 생성완료============
독수리이름의 조류가 날아요~
참새이름의 조류가 날아요~

 

-> birds.get[0]을 Sparrow에 넣는것은 불가능하다

birds라는건 결국 BIrd타입인데 Bird타입은 Sparrow보다 더 큰개념이고 더많은 정보를 가지고 있다

근데 Sparrow는 최소한 Bird가 가져야할 정보를 다 가지고있지 않기때문에, Sparrow의 다운케스팅은 안됨

그치만 자식인 Sparrow는 부모인 BIrd로 갈 수 있다 ( Sparrow(name) as Bird )  

자식이 가지고있던것은 부모로부터 온거기때문에 이건 가능 

-> 부모에서 자식으로 강제로 바꾸는건 안됨 / 자식은 부모로 갈수있음

 

 

 

 

# 자료형의 타입을 확인할 수 있다

 

- 코틀린은 is키워드를 활용해서 자료형의 타입을 확인할 수 있다

      if(name is String) {
        println("name은 String 타입입니다")
    } else {
        println("name은 String 타입이 아닙니다")
    }

 

 

 

 

# 여러 인스턴스를 리턴할 수 있다

 

- 메소드는 기본적으로 하나의 데이터를 리턴하지만, 두 개 이상의 데이터를 포함하는 데이터클래스를 설계하고 인스턴스를 리턴하면 가능하다

- 하지만 매번 불필요한 클래스를 만드는 행위는 비효율적이라서 따로 인스턴스를 리턴할 수 있는 키워드가 존재한다

(Pair와 Triple)

 

- 복수 데이터 리턴 방법

  • Pair (두개의 인스턴스 리턴)
    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
    }
}

//결과
달걀의 종류는 (달걀, 맥반석) 입니다.
리스트 달걀의 종류는 [달걀, 맥반석] 입니다.
첫번째 달걀의 종류는 달걀 입니다.
두번째 달걀의 종류는 맥반석 입니다.

 

 

  • Triple ( 세 개의 인스턴스 리턴 )
fun main() {
    var chicken = Chicken()
    var eggs = chicken.getThreeEggs()
    var listEggs = eggs.toList()
    
//    first, second, third로 관리
//    var firstEgg = eggs.first
//    var secondEgg = eggs.second
//    var eggTime = eggs.third
    
    // 리스트로 관리
    var firstEgg = listEggs[0]
    var secondEgg = listEggs[1]
    var eggTime = listEggs[2]

    println("달걀의 정보는 ${eggs} 입니다.")
    println("리스트 달걀의 정보는 ${listEggs} 입니다.")
    println("첫번째 달걀의 종류는 ${firstEgg} 입니다.")
    println("두번째 달걀의 종류는 ${secondEgg} 입니다.")
    println("달걀은 ${eggTime}에 나왔습니다.")
}

class Chicken {
    fun getTwoEggs(): Pair<String, String> {
        var eggs = Pair("달걀", "맥반석")
        return eggs
    }

    fun getThreeEggs(): Triple<String, String, Int> {
        var eggs = Triple("달걀", "맥반석", 20230101)
        return eggs
    }
}

//결과
달걀의 정보는 (달걀, 맥반석, 20230101) 입니다.
리스트 달걀의 정보는 [달걀, 맥반석, 20230101] 입니다.
첫번째 달걀의 종류는 달걀 입니다.
두번째 달걀의 종류는 맥반석 입니다.
달걀은 20230101에 나왔습니다.

 

 

 

 

# 자기 자신의 객체를 전달해서 효율적인 처리를 할 수 있다

 

- 코틀린에서는 Scope Functions들을 제공한다 

- 객체를 사용할때 시로 Scope를 만들어서 편리한 코드 작성을 도와준다

 

 

- Kotlin의 Scope Function 종류

 

  • 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 정리

- 수신객체와 람다함수간의 긴밀한 관계가 존재한다

 

- Scope Functions은 크게 두 가지로 구분할 수 있다 

   -> 명시적으로 수신객체 자체를 람다로 전달하는 방법

   -> 수신객체를 람다의 파라미터로 전달하는 방법

 

- 수신객체, 람다함수의 관계

   -> T는 수신객체를 의미한다

   -> block: 내부는 람다함수의 소스코드이다

   -> 수신객체는 it으로 사용할 수 있다

// 수신객체 자체를 람다의 수신객체로 전달하는 방법
public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <T> T.apply(block: T.() -> Unit): T
public inline fun <T, R> with(receiver: T, block: T.() -> R): R

// 수신객체를 람다의 파라미터로 전달
public inline fun <T> T.also(block: (T) -> Unit): T
public inline fun <T, R> T.let(block: (T) -> R): R

 

 

- 모든 수신객체를 it으로 활용하면 문제가 발생할 수 있다 

  -> 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
}

 

 

- 표로 정리!!

  Scope에서 접근방식 this Scope에서 접근방식 it
블록 수행 결과를 반환 run , with let
객체 자신을 반환 apply also

 

 

 

 

# 확장함수

- 기존 클래스에 쉽게 메소드를 추가할 수 있다

 

- 코틀린에서는 자바와 달리 외부에서 클래스의 메소드를 추가할 수 있다

- 과도하게 사용하면 코드의 가독성을 해칠 수 있지만 장점도 존재한다

- 원하는 메소드가 있지만 내가 설계한 클래스가 아닐때 외부에서 메소드를 관리한다

- 내 목적을 위해 외부에서 관리하기 때문에 원본 클래스의 일관성을 유지할 수 있다

 

# 주의사항

- 확장함수는 public 멤버에만 접근할 수 있고 private, protected는 접근할 수 없다

- private 또는 protected 멤버에 접근하려면, 클래스 내부의 멤버함수 형태가 적합하다

- 클래스의 멤버함수처럼 상속할 수 없다

- 즉, 하위 클래스에서 확장함수를 재정의(오버라이드)할 수 없다

fun String.isEmailValid(): Boolean {
    val pattern = "[a-zA-Z0-9._-]+@[a-z]+\\.+[a-z]+"
    return matches(pattern.toRegex())
}

 

 

- 예제 코드

-> 이름, 나이만 출력하는 displayInfo 메소드가 있는데 추가로 등급까지 조회 하고 싶다!

-> 클래스를 변경하지 못하는 상황에서, 확장함수로 메소드를 추가해서 사용할 수 있다

 

    -> X개발자가 클래스를 만들어서 전달해줬는데 나는 다른 기능도 추가되었으면 한다

    -> A개발자도 본인이 사용할 메소드가 추가적으로 필요하다고 함

    -> B개발자도 본인이 사용할 메소드가 추가적으로 필요하다고 함

    -> X개발자는 나름대로 확장성을 고려해서 클래스를 만들었는데.. 모든 요구를 들어주다가는 고려한 내용들을 지키지 못할것같다

    -> 이때!! 확장함수를 이용해서 필요한 기능들을 본인들이 추가해서 사용한다!!

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+ 입니다

 

 

 

 

 

# 비동기 프로그래밍 

- 순서대로 하나의 작업씩 수행하는 행위를 동기적 프로그래밍이라고 하는데, 동기적 프로그래밍은 순차적으로 수행하기 때문에 앞선 작업에 영향을 받는다

- 만약 앞선작업이 끝나지 않는다면, 뒷작업은 영원히 수행할 수 없다

- 꼭 동기적으로 실행하지 않아도되는 기능은 비동기적으로 실행하는게 좋다

 

ex)

- 5GB 영상 다운로드 → 메일전송 → 알림의 순서를 가진 로직이 있다고 해보자

- 해당 작업을 순차적으로 진행한다고 했을때, 다른 작업을 하지 못하고 앱이 멈추는 등의 문제가 발생할 수 있다

- 다른작업을 하고있다가 영상 다운로드가 완료되었을때 알림이 발생하면 좋을것같다 -> 이렇게 하기 위해서 비동기 프로그램을 사용

 

 

- 동기 프로그래밍

  • 요청을 보내고 결과값을 받을때까지 작업을 멈춘다
  • 한가지씩 작업을 처리한다

- 비동기 프로그래밍

  • 요청 보내고 결과값을 받을때까지 멈추지 않고 또 다른 일을 수행한다
  • 다양한 일을 한번에 수행한다 (효율적)

 

 

 

 

# 쓰레드

- 로직을 동시에 실행할 수 있도록 도와준다!

- 프로그램은 하나의 메인 쓰레드 (실행흐름)이 존재한다

- 하나의 메인 쓰레드는 —→ fun main() ←— 메인함수를 의미한다

- 여태까지는 메인 쓰레드위에서 로직을 실행해서 동시처리가 불가능했지만, 별도의 자식 쓰레드를 생성해서 동시에 로직을 실행할 수 있다 ( 즉, 우리가 여태까지 해왔던거는 main쓰레드 위에서 순차적으로 실행되는거였지만, main쓰레드 위에서도 별도의 자식 쓰레드들을 여러개 만들어서 동시처리를 할 수 있다 )

- thread 키워드로 쓰레드를 생성할 수 있다 

 

 

- 프로세스 (Process)

  • 프로그램이 메모리에 올라가서 실행될때, 이를 프로세스 1개라고 한다
  • 보통 프로그램을 더블클릭하면 프로세스가 생긴다

 

- 쓰레드 (Thread)

  • 쓰레드프로세스보다 더 작은 단위이다
  • 프로세스 안에서, 더 작은 작업의 단위를 쓰레드라고 한다 (하지만 최소 1개의 메인쓰레드는 존재함)
  • 쓰레드는 생성되서 수행할 때, 각 독립된 메모리 영역인 STACK을 가진다
  • 즉, 쓰레드를 한개 생성하면, 스택메모리의 일정 영역을 차지한다

 

 

# 메모리 자료

-> 하나의 프로세스 안에는, 여러개의 쓰레드들이 별도의 메모리 영역인 STACK을 가지고있다

 

 

# 쓰레드를 어디에 사용할까?

- 몬스터를 공격하고, 체력이 줄어들고, 효과음이 동시에 발생해야 할때

- 경마 프로그램의 말들이 동시에 출발해서 경쟁해야할때

 

 

# 예시 코드

일단 쓰레드를 사용하려면 외부종속성을 추가해줘야한다

https://github.com/Kotlin/kotlinx.coroutines#android   해당 링크를 참고해서 아래와같이 종속성을 추가해준다

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1-Beta")
}

 

 

- 1부터 10까지 출력하는 코드를 2개의 쓰레드로 경쟁시키는 코드

  • 1초마다 자원경쟁을 시키기 위해 runBlocking을 사용해서 1초 딜레이를 추가했다 
  • Thread1과 Thread2의 순서는 보장되지 않고 경쟁한다
fun main() {
    thread(start = true) {
        for(i in 1..10) {
            println("Thread1: 현재 숫자는 ${i}")
            runBlocking {
                launch {
                    delay(1000)
                }
            }
        }
    }

    thread(start = true) {
        for(i in 50..60) {
            println("Thread2: 현재 숫자는 ${i}")
            runBlocking {
                launch {
                    delay(1000)
                }
            }
        }
    }
}

//결과 (실행할때마다 달라짐)
Thread1: 현재 숫자는 1
Thread2: 현재 숫자는 50
Thread2: 현재 숫자는 51
Thread1: 현재 숫자는 2
Thread1: 현재 숫자는 3
Thread2: 현재 숫자는 52
Thread1: 현재 숫자는 4
Thread2: 현재 숫자는 53
Thread2: 현재 숫자는 54
Thread1: 현재 숫자는 5
Thread2: 현재 숫자는 55
Thread1: 현재 숫자는 6
Thread1: 현재 숫자는 7
Thread2: 현재 숫자는 56
Thread1: 현재 숫자는 8
Thread2: 현재 숫자는 57
Thread2: 현재 숫자는 58
Thread1: 현재 숫자는 9
Thread1: 현재 숫자는 10
Thread2: 현재 숫자는 59
Thread2: 현재 숫자는 60

 

-> 첫번째 Thread, 두번째 Thread, 이 자식 Tread 2개를 포함하는 메인Tread   =>  총 3개의 Tread

-> 쓰레드로 처리되기 때문에 둘이 동시에 처리됨 -> 즉, 1초마다 이 cpu자원을 할당한 애가 먼저 실행됨 

-> 1초마다 이 cpu자원을 할당한 애가 먼저 실행되기 때문에, 랜덤값이라고 볼 수 있음

 

 

 

 

# 코루틴

- 운영체제의 깊이있는 지식이 없어도 쉽게 비동기 프로그래밍을 할 수 있다

- 최적화된 비동기 함수를 사용한다 

- 하드웨어 자원의 효율적인 할당을 가능하게 한다

- 안정적인 동시성, 비동기 프로그래밍을 가능하게 한다

- 코루틴이 쓰레드보다 더욱 가볍게 사용할 수 있다 

- 로직들을 협동해서 실행하자는게 목표이고, 구글에서 적극 권장한다

 

- 코루틴 빌더의 종류

  • 일반적으로 launch와 async 빌더를 가장 많이 사용
  • launch는 결과값이 없는 코루틴 빌더를 의미

- lanuch는 Job객체로 코루틴을 관리한다

  • Job객체는 다양한 함수를 가지고 있다
  • join: 현재의 코루틴이 종료되기를 기다린다
  • cancel: 현재의 코루틴을 즉시 종료된다

- async는 결과값이 있는 코루틴이다

 

- 코루틴은 스코프로 범위를 지정할 수 있다

  • GlobalScope: 앱이 실행된 이후에 계속 수행되어야할때 사용한다
  • CoroutineScope: 필요할때만 생성하고 사용후에 정리가 필요하다

- 코루틴을 실행할 쓰레드를 Dispatcher로 지정할 수 있다

  • Dispatchers.Main: UI와 상호작용하기 위한 메인쓰레드
  • Dispatchers.IO: 네트워크나 디스크 I/O작업에 최적화되어있는 쓰레드
  • Dispatchers.Default: 기본적으로 CPU최적화되어있는 쓰레드

 

- 예제 코드 

 

-> 안드로이드는 항상 앱이 켜있는 상태라서 코루틴이 실행이되지만, 현재 실습환경은 실행 후 종료되는 JVM환경이다

-> 따라서 실행하면 main이 먼저 종료되기 때문에 코루틴의 결과를 얻을수없어서

runBlocking{ job.join()} 을 사용해서 강제로 job이 끝날때까지 기다리는 코드를 넣어준다 (실제 안드로이드 개발할때는 이 코드 써주지 않아도 코루틴 실행됨!)

 

fun main(args: Array<String>) {
    println("메인쓰레드 시작")
    var job = GlobalScope.launch {
        delay(3000)
        println("여기는 코루틴...")
    }
    runBlocking {
        job.join()
    }
    println("메인쓰레드 종료")
}

//결과
메인쓰레드 시작
여기는 코루틴...
메인쓰레드 종료

 

-> "메인쓰레드 시작"이 출력되고 끝나면, 3초후에 "여기는 코루틴..." 이 실행된다

 

 

- CoroutineScope를 이용해서 실행

fun main(args: Array<String>) {
    println("메인쓰레드 시작")
    var job = CoroutineScope(Dispatchers.Default).launch {
        delay(3000)
        println("여기는 코루틴...")
    }
    runBlocking {
        job.join()
    }
    println("메인쓰레드 종료")
		job.cancel()
}

//결과
메인쓰레드 시작
여기는 코루틴...
메인쓰레드 종료

 

 

 

- 여러개의 코루틴을 사용해서 실행 

-> 코루틴의 결과값을 리턴받을 수 있다

-> 결과값을 리턴받아야하기 때문에 await은 일시중단이 가능한 코루틴에서 실행가능하다

	println("메인쓰레드 시작")
    var job = CoroutineScope(Dispatchers.Default).launch {
        var fileDownloadCoroutine = async(Dispatchers.IO) {
            delay(10000)
            "파일 다운로드 완료"
        }
        var databaseConnectCoroutine = async(Dispatchers.IO) {
            delay(5000)
            "데이터베이스 연결 완료"
        }
        println("${fileDownloadCoroutine.await()}")
        println("${databaseConnectCoroutine.await()}")
    }
    runBlocking {
        job.join()
    }
    println("메인쓰레드 종료")
    job.cancel()
    
//결과
메인쓰레드 시작
파일 다운로드 완료(10초뒤)
데이터베이스 연결 완료(5초뒤)
메인쓰레드 종료

 

 

 

# 쓰레드와 코루틴 (정리)

- 쓰레드와 코루틴은 둘 다 동시성 프로그래밍을 위한 기술이다

 

 

# 쓰레드와 코루틴의 차이

 

- 쓰레드(Thread)

  • 각 작업 하나하나의 단위
  • 각 Thread가 독립적인 Stack 메모리 영역을 가진다
  • 동시성 보장수단 :  Context Switching
  • -> 운영체제 커널에 의한  Context Switching을 통해 동시성을 보장한다
  • 블로킹 (Blocking)
  • -> Thread A가 Thread B 의 결과를 기다리고 있다 
  • -> 이 때, Thread A블로킹 상태라고 할 수 있다
  • -> A는 Thread B 의 결과가 나올 때 까지 해당 자원을 사용하지 못한다

 

 

-> Thread A가 Task1을 수행하는 동안 Task2 의 결과가 필요하면 Thread B를 호출한다

-> 이때 Thread A는 블로킹 되고, Thread B로 프로세스간에 스위칭이 일어나 Task 2을 수행한다

-> Task 2가 완료되면 Thead A로 다시 스위칭해서, 결과 값을 Task 1에게 반환한다

-> 이때 Task 3, Task 4는 A, B작업이 진행되는 도중에 멈추지 않고 각각 동시에 실행되게 된다

-> 이때 컴퓨터 운영체제 입장에서는 각 Task를 쪼개서 얼마나 수행할지가 중요할것이다

-> 그래서 어떤 쓰레드를 먼저 실행해야할지 결정하는행위를 스케쥴링이라고 한다. 이런 행위를 통해 동시성을 보장한다

 

 

 

- 코루틴(Coroutine)

  • 각 작업 하나하나의 단위 :  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의 수행요청이 발생했다고 가정해보자

-> 이럴때 신기하게도 컨텍스트 스위칭 없이, 동일한 Thread A에서 수행할 수 있다

-> Thread C처럼 하나의 쓰레드에서 여러 Task Object들을 동시에 수행할 수 있다

-> 이러한 특징때문에 코루틴을 Light-Weight Thread라고 이야기한다

 

- Light-Weight Thread

  • 동시처리를 위해 스택영역을 별도로 할당하는 쓰레드처럼 동작하지 않는다
  • 그치만 동시성을 보장할 수 있다
  • 하나의 쓰레드에서 다수의 코루틴을 수행할 수 있다
  • 커널의 스케줄링을 따르는 컨텍스트 스위칭을 수행하지 않는다

 

- 요약

  • 쓰레드나 코루틴은 각자의 방법으로 동시성을 보장하는 기술이다
  • 코루틴Thread를 대체하는 기술이 아니다 (다른 개념이다) -> 하나의 Thread를 더욱 잘개 쪼개서 사용하는 기술이다
  • 코루틴은 쓰레드보다 CPU 자원을 절약하기 때문에 Light-Weight Thread라고 한다
  • 구글에서는 코틀린의 코루틴 사용을 적극 권장하고 있다