dev-course day60

2rlokr·2025년 5월 31일

dev-course

목록 보기
41/43
post-thumbnail

오늘 배운 것

제네릭 실습

case 1: list 생성

val a: List<Int> = listOf(1) // 읽기만 가능한 리스트
//    a.add(2)

val b : MutableList<Int> = mutableListOf(1) // 읽기와 쓰기가 가능한 리스트
b.add(2)
  • listOf로 생성된 리스트는 불변 리스트 (읽기만 가능) -> List<T>
  • mutableListOf로 생성된 리스트는 가변 리스트이다. (읽기와 쓰기가 가능) -> MutableList<T>

case 2: 스타 프로젝션

Kotlin (그리고 Java)에서는 제네릭 타입 정보는 런타임에 지워진다. -> Type Erasure(타입 소거)

즉, 두 타입은 런타임에서 구분할 수 없음: List<Int>, List<String>
→ 둘 다 그냥 List로만 인식됨.

fun <T> checkType(param : T) {
    if ( param is List<*> ) { // List<T> x !! 타입을 모르기 때문에 *를 써준다.
        println("param 은 리스트 타입입니다.")
    } else {
        println("param은 List 타입이 아닙니다.")
    }
}
  • is List<T>T가 런타임에 어떤 타입인지 모르기 때문에 검사 자체가 컴파일 에러가 뜬다.
  • T는 컴파일 타임에만 존재하고, 런타임에서는 List만 존재하게 된다.

코루틴 (Coroutine)

비동기 프로그래밍을 가능하게 해주는 개념으로, 일반 함수처럼 보이지만 중간에 실행을 멈췄다가 다시 시작할 수 있는 함수이다. 즉, 일시 중단(suspend)과 재개(resume)가 가능한 함수

루틴 (Routine)

루틴은 정해진 작업을 수행하는 코드 블록을 말한다. 일반적으로 함수나 메서드 형태로 사용된다. 루틴은 보통 재사용성과 구조화된 프로그램 작성을 돕기 위해 사용된다.

메인 루틴

  • 프로그램의 진입점이자 전체 흐름을 제어하는 루틴
  • 프로그램 실행 시 가장 먼저 호출됨
  • 다른 서브 루틴들을 호출하고 실행 순서를 관리함

서브 루틴

  • 메인 루틴에서 호출되는 보조적인 작업 단위
  • 작업(기능)을 수행하고, 필요하면 결과를 반환함
  • 다른 루틴에서도 재사용 가능

메모리 관점

힙 사용

코루틴은 스택 대신 힙을 사용해 상태를 저장한다.

  • 일반 함수(서브 루틴)는 스택 프레임을 생성해서 실행되지만, 코루틴은 suspend 키워드를 만나며 현재 상태를 힙에 저장하고 스레드를 반환한다.

일시 중단 시 스레드

일시 중단 시 스레드를 차지하지 않는다.

  • 코루틴이 delay, IO withContext(IO) 같은 suspend 지점을 만나면,
    • 실행을 멈추고 스레드를 반환한다. (스레드는 다른 일 처리 기능)
    • 코루틴 객체는 메모리에 남아 있는다.

그래서, 스레드보다 코루틴이 가볍다고 하는 것이다.

코루틴 실습

case 1 : 일반 Thread 사용

class CoroutineThread() : Thread() {
    override fun run() {
        println("수행할 명령입니다! 현재 쓰레드 : ${Thread.currentThread().name}")
    }
}

class CoroutineThread2: Runnable {
    override fun run() {
        println("수행할 명령입니다! 현재 쓰레드 : ${Thread.currentThread().name}")
    }
}

val thread1: Thread = CoroutineThread()
thread1.start()

val thread2 = Thread(CoroutineThread2())
thread2.start()

val thread3 = Thread {
	println("수행할 명령입니다! 현재 쓰레드 : ${Thread.currentThread().name}")
}
thread3.start()

val thread4 = object : Thread() {
	override fun run() {
    	println("수행할 명령입니다! 현재 쓰레드 : ${Thread.currentThread().name}")
    }
}
thread4.start()

// thread start = true로 하면 바로 실행된다.
thread(start = false, name="성찬이 쓰레드") {
	println("수행할 명령입니다! 현재 쓰레드 : ${Thread.currentThread().name}")
}.start()
  • Thread() 클래스나 Runnable 인터페이스를 구현한 구현체를 이용해 쓰레드를 실행시킬 수 있다.
  • thread(start=false, name = "{쓰레드 이름}") 방식으로도 쓰레드를 실행할 수 있다.
  • 이 방법들은 모두 실제로 OS 스레드를 사용하는 것이다.

case 2 : 쓰레드 풀

val threadPool = Executors.newFixedThreadPool(5)
  • OS에서 최대 5개의 스레드를 요청해서 풀에 보관한다.
    즉, 스레드 5개를 미리 만들어두고, 작업이 들어오면 거기서 꺼내 쓰는 것이다.
val services = List(100) { idx ->
	{
    	println("작업 ${idx}번 시작 - 사용된 쓰레드 : ${Thread.currentThread().name}")
        Thread.sleep(2000)
        println("작업 ${idx}번 종료 - 사용된 쓰레드 : ${Thread.currentThread().name}")
    }
}

for (logic in services) {
	threadPool.submit { // 놀고 있는 스레드가 있다면 logic()을 실행하라고 하는 것
    	logic()
    }
}
  • 함수 객체를 리스트에 넣고, threadPool.submit을 이용하여 함수를 실행한다.
  • 미리 만들어진 스레드풀(threadPool) 에게 작업을 맡겨, 놀고 있는(Idle) 스레드가 있다면 즉시 실행한다.
  • 만약, 놀고 있는 스레드가 없다면 작업 큐에 넣어 대기한다.
  • logic() 함수가 스레드에서 실행된다.
threadPool.shutdown()
  • 스레드풀에게 더 이상 새로운 작업은 받지 말라고 요청하는 것이다.
  • 이미 제출된 작업은 모두 실행하고 마무리한다. 즉, 현재 큐에 있는 작업은 다 끝낼 때까지 기다리게 된다.

case 3 : runBlocking

fun main() = runBlocking<Unit> {
  • 코루틴 블로킹 실행으로, runBlocking은 메인 쓰레드에서 코루틴을 시작하고, 코루틴이 모두 끝날 때까지 메인 쓰레드를 블로킹하게 된다.
  • 비동기 작업을 동기적으로 기다리게 해주는 함수예요
  • 즉, 내부 작업이 끝날 때까지 main 함수 종료를 막는 장치

case 4 : GlobalScope & launch

fun main() {

    GlobalScope.launch { 
        println("현재 스코프 : ${this.coroutineContext}")
        println("현재 쓰레드 : ${Thread.currentThread().name}")
        // 현재 쓰레드 : DefaultDispatcher-worker-1
    }

    println("Hello, ")

    Thread.sleep(3000) // 기다려줘야지만 GlobalScope 흐름이 실행되는 것을 볼 수 있다.
}
  • 실제 코루틴 작업이 실행되기 전까지 약간의 시간이 걸릴 수 있다.
  • 그 이유는 코루틴을 실행할 때 내부적으로는 Dispatchers.Default가 사용되고,
    Dispatchers.DefaultCPU 코어 수만큼 미리 만들어진 스레드 풀(Worker Thread Pool)에서 실행된다. (이 스레드 풀은 코틀린 런타임이 관리한다.)
  • 이 스레드 풀은 프로그램 처음 코루틴을 쓸 때 생성되고, 그래서 첫 실행 때는 스레드 풀을 만드는 초기 비용 때문에 살짝 딜레이가 있을 수 있다.

GlobalScope

  • GlobalScope는 전역 코루틴 스코프로, app 전체에서 공통으로 사용하는 전역 생명주기를 따라가는 가장 넓은 스코프이다.
  • GlobalScopeDefaultContext를 가진다.
  • 즉, 이 안에서 실행된 코루틴은 애플리케이션 종료 시까지 살아있고, 어디서나 접근 가능해요.
  • 하지만 관리가 어렵고, 메모리 누수 위험이 있기 때문에 보통은 권장하지 않고, 대신 수명 주기를 직접 관리하는 스코프 (예: CoroutineScope)를 쓰는 게 좋다.

launch

  • launch는 코루틴을 새로운 작업 단위(코루틴)로 실행하는 함수이다.
  • launch는 즉시 실행되며, 결과값이 없고 작업이 끝날 때까지 기다리지 않아도 된다. (비동기).
  • launchJob 객체를 반환해서, 나중에 작업을 취소(cancel())하거나 완료 대기(join())를 할 수 있다.

case 5 : runBlocking vs GlobalScope

GlobalScope

	GlobalScope.launch { // async는 뭔가 반환할 때 쓴다.
		println("[2] GlobalScope.launch 진입 - 현재 쓰레드 : ${Thread.currentThread().name}")
    	repeat(3) {
    		delay(500)
        	println("[3] GlobalScope 내부 작업 처리중.. ${it}")
    	}
    	println("[4] GlobalScope.launch 종료")
	}
	println("[5] launch 호출 직후!")
}

Thread.sleep(10000)
  • GlobalScope은 main함수의 생명주기를 따라가기 때문에 Thread.sleep(10000)을 해주지 않으면 코루틴 내부의 동작을 하지 못하고 중단된다.
  • 또한, 비동기적으로 실행되기 때문에, [5] launch 호출 직후!가 '[2],[3],[4]' 보다 빨리 출력된다.

runBlocking

threadPool.submit {

	println("[1] 작업 시작 - 현재 쓰레드 : ${Thread.currentThread().name}")

	runBlocking { // async는 뭔가 반환할 때 쓴다.
    	println("[2] runBlocking.launch 진입 - 현재 쓰레드 : ${Thread.currentThread().name}")
        repeat(3) {
        	delay(500)
            println("[3] runBlocking 내부 작업 처리중.. ${it}")
        }
        println("[4] runBlocking.launch 종료")
    }
    println("[5] launch 호출 직후!")
}
  • runBlocking내부 코드를 코루틴 스코프에서 동기적으로 실행하며, 실행이 끝날 때까지 현재 스레드를 블로킹한다.
  • 때문에 [1] ~ [5] 까지 순차적으로 출력되며, Thread.sleep()을 하지 않아도 로직이 다 실행되고 main 함수가 종료된다.

case 6 : Job

val result: Job = GlobalScope.launch {
	delay(1000L)
    println("World!")
}

println("Hello,")
println("job.isActive : ${result.isActive}, completed: ${result.isCompleted}")
Thread.sleep(2000L)
println("job.isActive : ${result.isActive}, completed: ${result.isCompleted}")
  • launchJob을 반환하는데, 이 isActive, isCompleted 로 이 Job의 실행여부, 완료 여부를 확인할 수 있다.

case 7 : join()


fun main() = runBlocking{

    val result1: Job = launch {
        delay(500)
        println("World!")
    }

    val result2: Job = launch {
        delay(2500)
        println("World!")
    }

    println("Hello,")

    result1.join()
    result2.join()
}
  • launch 두 개가 비동기적으로 실행된다.
    → 둘 다 즉시 시작하지만 내부에서 delay()를 걸고 멈춰 있다.
  • 그래서 "Hello,"가 먼저 실행되고, 이후 join()으로 결과를 기다렸다가 결과를 출력하게 된다.

case 8 : async

fun main() = runBlocking<Unit>{
    
    val asyncValue: Deferred<String> = async {
        delay(5000)

        "결과값"
    }

//    println(asyncValue) // 이렇게 하면 delay 때문에 기다리다가 종료되기 때문에 결과값을 받아올 수 없다.
    println(asyncValue.await())
    
}
  • async는 비동기적으로 실행되는 코루틴을 만들고, 결과값을 반환할 수 있도록 한다.
  • runBlocking이라 main 함수가 async 가 완료되기 전에 종료되진 않지만, 비동기적으로 실행하는 중에 println(asyncValue)를 만나면, 아직 결과값이 반환되기 이전이기 때문에 결과값을 받아올 수 없게 된다.
  • 결과값을 기다렸다가 받아오고 싶을 땐 asyncValue.await()을 사용할 수 있다.

case 9 : suspend 함수

suspend fun fetchData() : String{
    delay(5500)
    return "받아온 데이터!"
}

fun main() = runBlocking<Unit>{

//    println(fetchData()) // 얘를 이렇게 실행하면 5.5초 기다리기 때문

    val result = async { fetchData() }

    launch {
        repeat(5) { i ->
            delay(1000)
            println("작업 처리 중... ${i+1}초 경과")
        }
    }

    println("데이터 받아오는 중")
    println(result.await())
}
  • suspend함수가 일시 중단(suspend)될 수 있다는 걸 나타내는 키워드이다.
  • 즉, 이 함수는 실행을 잠깐 멈췄다가, 나중에 이어서 실행할 수 있어요.
  • 코루틴 안에서만 호출할 수 있다.
  • suspend 함수를 직접 호출하면, 해당 코루틴이 그 함수가 끝날 때까지 다음 줄로 못 넘어간다. 그렇기 때문에 println(fetchData()를 하면 5.5초를 기다렸다가 다음 코드가 실행된다.
  • 그래서 async를 통해 비동기적으로 실행하는 것이다.

case 10 : 코틀린 sequence에서의 yield

fun main() = runBlocking<Unit> {

    val numbers = sequence {
        println("yield(1)전")
        yield(1)
        println("yield(1)후")
        println("yield(2)전")
        yield(2)
        println("yield(2)후")
        println("yield(3)전")
        yield(3)
        println("yield(3)후")
    }

    launch {
        for(num in numbers) {
            println(num)
            delay(5000)
        }
    }
}

sequence

  • sequence지연 시퀀스(lazy sequence)를 만드는 함수이다.
  • 내부에서 yield를 사용해 값들을 순차적으로 생성한다.
  • 한 번에 모든 값을 미리 만들어놓는 것이 아니라, 필요할 때마다 하나씩 값을 만들어서 반환합니다.
    즉, "필요한 시점까지 계산을 미룸" → 메모리 절약과 효율적인 계산이 가능하게 된다.

yield

  • yieldsequence 빌더 내부에서 값을 반환하는 키워드이다.
  • yield(값)은 지금 바로 값을 반환하고 시퀀스 생성은 일시 중단한다.
  • 다음 값이 필요할 때, 그 시점부터 다시 시퀀스 생성이 재개된다.

case 11 : 코틀린 yield 함수

fun main() = runBlocking<Unit> {

    launch {
        repeat(3) {
            println("작업 A 시작!")
            yield()
            println("작업 A 끝")
        }
    }

    launch {
        repeat(3) {
            println("작업 B 시작!")
            yield()
            println("작업 B 끝")
        }
    }
}
  • 코루틴에서 쓰이는 yield()라는 suspend 함수이다.
  • 현재 코루틴의 실행을 잠시 멈추고, 다른 코루틴에게 실행 기회를 넘기는 역할을 한다.
  • yield()sequence랑 다르게 코루틴 컨텍스트에서만 의미가 있다.

case 12 : 동시성 문제 (race condition)

  • 동시성 문제 : 여러 코루틴이 동시에 같은 자원(예: 변수, 데이터)에 접근해서 값을 읽거나 쓸 때, 실행 순서에 따라 결과가 달라지는 문제
  • 예를 들어, 두 코루틴이 count++를 동시에 하면 예상보다 증가가 덜 될 수 있다.

case 13 : Atomic 변수 사용

var generalCount: Int = 0
var atomic = AtomicInteger(0)

suspend fun process1(action : suspend () -> Unit) {
    var i = 100
    var j = 100

    val time = measureTimeMillis {
        val services = List(i) {
            GlobalScope.launch {
                repeat(j) {
                    action()
                }
            }
        }
        services.forEach { it.join() }
    }
    println("${i * j}만큼의 작업을 수행하는데 ${time}ms 만큼의 시간이 소요되었습니다.")
}

fun main() = runBlocking<Unit> {
    process1 {
        generalCount++
    }
    println("generalCount=$generalCount") // 10000이 안 나온다. 
    process1 {
        atomic.incrementAndGet()
    }
    println("atomic=$atomic") // 10000 나옴.
}
  • 상황 : 100개의 코루틴이 100번의 action 함수를 runBlocking 스코프에서 실행하고 있는 것이다.
  • 100개의 코루틴이 동시에 비동기적으로 generalCount에 쓰기 작업을 하게 되면 원하는 결과값이 나오지 않게 된다.
  • AtomicInteger을 사용하여 동시성 문제를 해결할 수 있다.

case 14 : 단일 스레드 컨텍스트

val context = newSingleThreadContext("GreppContext")
var contextCount: Int = 0

suspend fun process2(action : suspend () -> Unit) {
    var i = 100
    var j = 100

    val time = measureTimeMillis {
        val services = List(i) {
            GlobalScope.launch {
                repeat(j) {
                    action()
                }
            }
        }
        services.forEach { it.join() }
    }
    println("${i * j}만큼의 작업을 수행하는데 ${time}ms 만큼의 시간이 소요되었습니다.")
}

fun main() = runBlocking<Unit> {

    process2 {
        println("쓰레드 - ${Thread.currentThread().name}")
        withContext(context) {
            println("쓰레드 - ${Thread.currentThread().name}")
            contextCount++
        }
    }
    println("generalCount=$contextCount")
}
  • newSingleThreadContext로 단일 스레드 컨텍스트를 생성한다.
  • withContext(context)로 해당 로직은 하나의 스레드만 들어가서 실행할 수 있게 되는 것이다.
  • 즉, 여러 스레드가 contextCount 변수에 접근하지 못하게 되는 것이다.

case 15 : 상호 배제 mutex

val mutex = Mutex()
var mutexCount = 0
suspend fun process3(action : suspend () -> Unit) {
    var i = 100
    var j = 100

    val time = measureTimeMillis {
        val services = List(i) {
            GlobalScope.launch {
                repeat(j) {
                    action()
                }
            }
        }
        services.forEach { it.join() }
    }
    println("${i * j}만큼의 작업을 수행하는데 ${time}ms 만큼의 시간이 소요되었습니다.")
}

fun main() = runBlocking<Unit> {

    process3 {
        mutex.withLock {
            mutexCount++
        }
    }
    println("mutexCount=$mutexCount")
}
  • 여러 코루틴(또는 스레드)이 하나의 공유 자원(변수 등)에 동시에 접근하지 못하도록 잠금(lock) 을 걸어줘서 데이터가 꼬이거나 이상해지는 걸 막아주는 역할을 한다.
  • withLock { ... } 안에 있는 코드 블록은 한 번에 하나의 코루틴만 실행할 수 있게 만든다고 볼 수 있다.

팀 활동 후기

오늘은 RBF가 있는 날이었다. 나는 어제 강사님의 질문에.. 스터디로 공부한 건 기억나지만 희미하게 기억나고.. 이걸 말로 설명할 수 없음에 슬픔을 느끼고 주제를 '자바 프로그램 실행 과정 & JVM 구조'로 잡고 발표했다. 확실히 한 번 더 보니까 이해가 잘 되는구만 !!! CS는 정말 한 번 이해해도 금방 까먹어서 너무 슬픈 것 같다. 근데 이게 장기 지식으로 가기 위해서는 반복밖에 없는 것 같다. ㅠ
각자 다양한 주제를 가져와줬는데, 되게 유익했던 것 같다.
특히, Virtual Thread 주제가 되게 유익했다. 사실 운체 공부가 부족하다는 사실은 코루틴 때 너무 많이 느껴버려서.. 어렵다.. 이러고 있었는데 Virtual Thread를 주제로 설명해주셔서 스레드에 대한 이해와 멘토링 시간에 들었던 Virtual Thread가 더 이해가 잘 됐던 것 같다 !! 그리고 어려운 것 같은데 이걸 우째 이렇게 쉽게 설명해주시지.. 하는 대단함..
RBF 때문에 공부하신 게 아니라 멘토링 이후에 궁금해서 찾아보셨다고 하는 것도 대단했다. 다른 분들의 발표도 그렇고, 열심히 하시는 걸 보면 자극이 된다 ! 나도 열심히 해야지..

느낀점

코루틴... 정말 열심히 따라가보려고 했으나 한 14...15 때부터 희미해졌다. 이해가 된 듯했지만, 그 다음 예시를 보니까 또 내가 이해한 게 맞나? 하는 너낌스.. 뭔가 virtual thread 설명도 듣고, 코드를 다시 보니까 더 이해가 잘된 것 같다. 그리고 운체가 부족하다는 것을 너무 크게 느꼈다. 멘토링 때도 느꼈는데 코루틴 수업 이후에는 더 느꼈다. ㅎㅅㅎ 스터디에서 운체 시작했으니까.. 똑띠 공부해야지 !!!!
코루틴 이해하니까 재밌긴 하네.. 하지만, 코드를 보고 좀 생각해야한다는 점 하하
벌써 코틀린 이론은 다 배웠다 ! 신기하네.. 일주일만에.. 허허

0개의 댓글