1. suspend 함수
- 코틀린 코루틴에서 비동기적으로 실행할 수 있는 특수한 함수로
- 코루틴 내에서 중단 가능(suspension) 한 작업을 처리할 수 있도록 설계됨
- 실행 일시 중단 가능
- 나중에 다시 실행 재개할 수 있는 특징 지님
- 코루틴 내에서 네트워크 요청이나 파일 읽기/쓰기와 같이 특정 작업을 대기해야 할 때 CPU 리소스를 낭비하지 않고, 효율적으로 다른 작업 처리 가능
1.1 suspend 함수 특징
- 코루틴 내에서만 실행 가능: suspend 함수는 오직 코루틴 내에서만 호출될 수 있으며 코루틴 범위 밖에서는 직접 호출 불가
- 중단 및 재개 가능: 앞서 언급했다시피 suspend 함수는 중간에 실행을 멈추고, 나중에 다시 이어서 실행 가능
- 비동기 작업 처리: suspend 함수는 일반 함수와 달리 비동기 작업을 처리할 수 있으며 이는 시간이 오래 걸리는 I/O 작업에 매우 적합
1.2 suspend 함수 vs 일반 함수
특징 | suspend 함수 | 일반 함수 |
실행 방식 | 비동기 중단 및 재개 가능 |
동기 호출이 완료될 때까지 대기 |
실행 스코프 | 코루틴 내에서만 호출 가능 | 코루틴 외부에서 호출 가능 |
CPU 점유 | 작업이 완료될 때까지 중단하고 다른 작업 실행 가능 | 작업 중 CPU를 계속 점유 |
blocking 여부 | 호출 쓰레드를 블로킹하지 않음 non-blocking |
호출 쓰레드를 블로킹 blocking |
1.3 코틀린 코드를 자바 코드로 변환한 뒤 차이점 확인
- Intellij에서 코틀린 코드를 bytecode로 변환한 뒤 Decompile을 통해 bytecode를 자바 코드로 변환 가능
1.3.1 코틀린 코드
- 코틀린 코루틴과 Reactor의 Mono를 함께 사용하는 간단한 예제로 코틀린의 suspend 함수와 Reactor의 비동기 스트림 API를 결합하여 비동기적으로 데이터를 처리하는 방식
1.3.2 변환된 자바 코드
- greet 메서드에 Continuation이라는 인자가 추가된 것을 확인 가능
- ContinuationImpl에는 label과 result가 포함되었고
- switch 문을 통해 label에 따라서 코드가 수행되는 것을 확인 가능
1.3.3 코틀린 코드에서 자바 코드로 변환 결과
- Continuation 인자 추가 및 Continuation 구현체 생성
- switch문 추가
- DelayKt.delay를 호출하며 Continuation 전달하고 종료
- System.out.println 수행
- Unit 반환
* 코틀린 컴파일러가 suspend 함수를 변환하는 과정을 파악하기 위해서는 FSM(Finite State Machine)과 Continuation Passing Style에 대한 이해 필요
2. Finite State Machine
- 유한한 개수의 상태를 갖는 state machine으로 한 번에 오직 하나의 상태만을 가질 수 있음
- 이벤트를 통해 하나의 상태에서 다른 상태로 전환 가능
- 프로그래밍에서는 복잡한 제어 흐름을 관리하기 위해 FSM을 사용
2.1 FSM 구현 방법
- label을 이용해서 when 문을 수행
- 각각의 case에 작업을 수행하고 label을 변경
- 재귀함수를 호출하며 다음 label을 전달하여 상태 변경
- label을 직접 인자로 넘기는 대신 Shared라는 data class를 통해 전달
- 추가로 Shared 내 result 인자에 계산된 결과를 저장하여 재귀함수에 전달
3. Continuation Passing Style
- 함수의 실행이 끝난 후 무엇을 할지에 대한 정보를 Continuation이라는 추가 인자로 전달하는 스타일
- 함수가 직접 결과를 반환하는 대신, 결과를 다음으로 처리할 함수에 넘겨주는 방식
- 값을 반환하는 대신 Continuation을 실행
3.1 Continuation 인터페이스
- 코틀린 코루틴에서 Continuation 인터페이스 제공
- resumeWith를 구현하여 외부에서 해당 Continuation을 실행할 수 있는 엔드 포인트 제공
- 이를 위해 CoroutineContext 포함
3.2 Continuation vs Callback
특징 | Continuation | Callback |
사용 방식 | suspend 함수에서 사용 코루틴의 중단과 재개에 쓰임 |
함수 호출 시 콜백 함수를 전달하여 작업 완료 시 호출 |
context 유지 | 함수 상태를 유지하고 중단된 지점에서 재개 가능 | 함수 상태 유지 X 완료되면 콜백 호출 |
가독성 | 비동기 코드를 동기식 코드처럼 간결하게 작성 가능 | 콜백 함수가 중첩되면 복잡도 올라감 |
비동기 코드 흐름 | 중단점에서 재개 | 작업 완료 시점에서 콜백 호출 |
예외 처리 | try-catch로 처리 가능 코루틴에서 자체 지원 |
콜백 내부에서 에러 처리를 명시적으로 처리 |
3.3 FSM에 CPS를 적용한 예제
4. 동기 코드를 코루틴으로 전환하는 과정 예시
4.1 동기 코드
- 고객, 상품, 스토어, 그리고 주소 정보 조회 후 주문을 넣는 mock 코드
4.2 비동기 코드로 전환
- CompletableFuture, Reactive Streams, 그리고 Flow 등을 이용하여 비동기 코드로 전환
- 하지만 비동기 코드로 전환함에 따라 가독성이 떨어짐
- 아도겐 코드와 같은 형태가 되어 코드 복잡도 올라감
- 동기 코드에 비해 가독성이 떨어짐
4.3 비동기 코드에 FSM 적용
- data 클래스인 Shared 객체 정의
- main에서 최초로 실행하는 경우 null이 전달되기 때문에 Shared 객체 생성
- shared의 label 값에 따라 다른 case문을 실행하며 case 문 내에서는 label을 변경하고 이전에 실행됐던 결과를 shared 내부의 중간값에 저장
- 각 case 문마다 cont.result에 값을 저장 후 재귀 호출
4.3.1 FSM만 적용했을 때의 문제점
- cont.result에 값을 넣고 재귀 함수를 호출하는 코드가 반복됨
- 재귀 함수를 직접 호출하기 때문에 코드를 외부로 분리하기 힘듦
- main 함수에서는 Shared 객체를 생성하지 않기 때문에 결과를 출력하는 부분을 하드 코딩
- 개선하기 위해서는 Continuation을 전달하는 형태로 변경해야 함
4.4 비동기 코드에 CPS 적용
- Shared 객체를 CustomContinuation 객체로 변경
- CustomContinuation은 main으로부터 Continuation을 받음
- 중간값들 뿐만 아니라 매개변수들과 인스턴스까지 Continuation에 저장
- 가장 마지막 state에서 complete를 호출하여 Completion 호출
* 여전히 재귀 함수를 호출하는 코드가 반복됨
4.5 비동기 코드에서 재귀 함수 반복 호출 제거
- subscribe, thenAccept 후 cont::resume을 넘기는 부분을 Continuation을 인자로 받는 확장 함수로 분리 가능
- 분리할 경우 CompletableFuture, Flowable을 사용할 때는 더 이상 직접 subscribe, thenAccept 등을 사용하지 않더라도 확장 함수를 통해 재귀 함수 반복을 피할 수 있음
4.6 코루틴 적용
- 코틀린 컴파일러가 하는 일을 역순으로 수행함으로써 FSM, CPS이 적용된 비동기 코드를 코루틴 코드로 전환
- 코루틴을 사용하면 비동기 영역에서 결과가 반환될 때까지 일시 중단하고 결과가 반환되면 재개 가능 (suspendable)
- 이처럼 코드를 일시 중단하고 재개 가능한 단위를 코루틴이라고 지칭
부연 설명
- 코틀린 컴파일러는 suspend가 붙은 함수에 Continuation 인자 추가
- 다른 suspend 함수를 실행할 경우 소유하고 있는 Continuation 전달
- 이러한 변환으로 인해 앞서 언급했다시피 suspend가 없는 함수는 전달할 Continuation이 없기 때문에 다른 suspend 함수 호출 불가
- suspend 함수 내부를 when 문을 이용해서 FSM 상태로 변경
- 각각의 state에서는 label을 변경하고 비동기 함수를 수행
- 비동기 함수가 완료되면 continuation.resume을 수행하여 다시 복귀하지만 label이 변경되면서 다른 state로 전환
- 마지막 state에 도달하면 completion.resume을 수행하고 종료
5. Spring WebFlux에서 코루틴 적용
- 앞서 언급했다시피 suspend 함수는 suspend 함수나 코루틴 내부가 아니라면 실행 불가
- 하지만 Spring WebFlux는 다음과 같은 케이스에 대해서도 suspend 함수를 호출할 수 있도록 지원
- Controller 내부에서 suspend 함수를 호출해야 하는 케이스
- 변경 불가능한 interface가 Mono나 CompletableFuture 등을 반환하고 suspend 함수가 아닌 케이스
5.1 Spring WebFlux suspend 함수 지원
- Spring WebFlux는 suspend 함수 지원
- Context1, MonoCoroutine, Dispatchers.Unconfined를 context로 갖고 reactor-http-nio-2 쓰레드에서 실행
- RequestMappingHandlerAdapter가 handlerMethod를 실행하고 handlerMethod로부터 invocableMethod를 획득하고 invoke를 통해 실행
- 주어진 메서드가 suspend 함수인지 체크하고 suspend 함수가 맞다면 CoroutineUtils.invokeSuspendingFunction을 실행하고 아니라면 method.invoke 실행
- invokeSuspendingFunction 내부에서 mono를 실행하며 Dispatchers.Unconfined를 전달
- Flow를 반환한다면 mono.flatMapMany를 사용
5.2 외부 라이브러리에서 제공되는 인터페이스가 Mono를 반환하는 경우 suspend 함수 호출하는 방법
- kotlin-coroutines-reactor에서 mono 함수를 제공
- mono 함수를 이용해서 내부에서 suspend 함수 실행
- mono 함수의 결과값은 Mono이기 때문에 그대로 반환
- monoInternal에서는 sink로부터 ReactorContext를 추출
- 추출한 ReactorContext로 CoroutineContext를 생성
- MonoCoroutine을 생성하고 시작
- MonoCoroutine은 sink를 인자로 받고 Coroutine이 complete 되면 sink의 success를 호출
- 반대로 cancel 된다면 sink의 error를 호출
5.3 CompletableFuture를 반환하는 함수에서 suspend 함수를 호출하는 방법
- CoroutineScope를 생성하고 해당 스코프에서 Future를 실행하여 suspend 함수를 실행 후 결과를 CompletableFuture 형태로 반환
참고
패스트 캠퍼스 - Spring Webflux 완전 정복 : 코루틴부터 리액티브 MSA 프로젝트까지
반응형
'Kotlin' 카테고리의 다른 글
[Kotlin] CoroutineScope (0) | 2024.10.03 |
---|---|
[Kotlin] CoroutineContext (0) | 2024.10.03 |
[Kotlin] 코루틴 (Coroutine) 개요 (0) | 2024.10.02 |
[Kotlin] lateinit과 by lazy의 차이점 (2) | 2022.07.13 |