코틀린(Kotlin)/TIL

[Kotlin 문법] 코틀린 심화

초보왕보초 2023. 12. 1. 19:50
728x90
  • 유용한 기능
  • 확장함수
  • 비동기 프로그래밍
  • 쓰레드
  • 코루틴
  • 쓰레드와 코루틴

 

 

 

유용한 기능

자료형을 변환할 수 있다

일반 자료형 간의 변환 예시

    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은 크게 두 가지로 구분할 수 있다
  1. 명시적으로 수신객체 자체를 람다로 전달하는 방법
  2. 수신객체를 람다의 파라미터로 전달하는 방법
  • 수신객체는 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의 결과가 나올 때까지 해당 자원을 사용하지 못한다

예시)

  1. Thread A가 Task 1을 수행하는 동안 Task 2의 결과가 필요하면 Thread B를 호출
  2. 이때, Thread A는 블로킹되고 Thread B로 프로세스 간의 스위칭이 일어나 Task 2를 수행한다
  3. Task 2가 완료되면 Thread A로 다시 스위칭해서 결괏값을 Task 1에게 반환한다
  4. 이때, Task 3, Task 4는 A, B작업이 진행되는 도중에 멈추지 않고 각각 동시에 실행한다
  5. 이때, 컴퓨터 운영체제 입장에서는 각 Task를 쪼개서 얼마나 수행할지가 중요하다
  6. 그래서 어떤 쓰레드를 먼저 실행해야 할지 결정하는 행위를 스케쥴링이라고 한다
  7. 이러한 행위를 통해 동시성을 보장한다

 

코루틴

  • 작업 하나하나의 단위: 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에서 실행된다

예시)

  1. Coroutine은 작업 단위가 Object이다
  2. Task 1을 수행하다가 Task 2의 수행요청이 발생했다면
  3. Context Switching 없이 동일한 Thread A에서 수행할 수 있다
  4. Thread C처럼 하나의 쓰레드에서 여러 Task Object들을 동시에 수행할 수 있다
  5. 이러한 특징 때문에 코루틴을 Light-Weight Thread라고 한다
    - 동시처리를 위해 스택영역을 별도로 할당하는 쓰레드처럼 동작하지 않는다
    - 그러면서도 동시성을 보장할 수 있다
    - 하나의 쓰레드에서 다수의 코루틴을 수행할 수 있다
    - 커널의 스케쥴링을 따르는 Context Switching을 수행하지 않는다

요약

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