코루틴과 suspend로 간단한 비동기 처리
📌 서론
이 글에서는 코루틴과 suspend 함수를 사용하여 복잡한 비동기 작업을 간단하게 처리하는 방법을 정리해보려고 한다.
코루틴(Coroutines) 기본
코루틴은 비동기 프로그래밍을 단순화하는 코틀린의 기능으로, 비동기 작업을 동기 작업처럼 쉽게 작성할 수 있게 해 준다. 코루틴을 사용하면, 복잡한 콜백(callbacks)이나 별도의 스레드 관리 없이 비동기 코드를 작성할 수 있다.
비동기 프로그래밍을 단순화하는 코틀린의 기능으로, 경량 스레드(lightweight threads)라고도 불린다. 그리고 비동기 작업을 동기 작업처럼 쉽게 작성할 수 있게 해준다. 코루틴을 사용하면, 복잡한 콜백(callbacks)이나 별도의 스레드 관리 없이 비동기 코드를 작성할 수 있다.
경량 스레드라는 용어는 코루틴이 기존 스레드에 비해 훨씬 적은 리소스(메모리 등)를 사용하여 마치 많은 수의 작업을 동시에 처리하는 것처럼 보이게 한다는 의미다.
만약에 카페에서 바리스타 한 명이 일한다고 가정해보자. 이 바리스타(코루틴)는 여러 커피 주문(작업)을 동시에 처리할 수 있으며, 한 번에 하나의 커피만 만들 수 있는 일반적인 바리스타(전통적인 스레드)보다 훨씬 효율적으로 일할 수 있다. 커피 머신을 사용하는 동안 다른 주문의 준비를 계속한다. 이처럼 코루틴은 작업 중 일부가 대기 상태일 때, 그 시간을 효율적으로 다른 작업을 처리하는 데 사용하여 자원을 절약한다. 그래서 코루틴을 사용하면 비동기 작업을 쉽게 처리하고, 코드의 복잡성을 줄일 수 있다.
또한 코루틴은 비동기 작업을 효율적으로 관리하고 실행 순서를 유연하게 제어할 수 있게 해주며, 이는 애플리케이션의 성능과 반응성을 향상시킨다. 비동기 순서를 유연하게 제어한다는 것은, 코루틴을 사용하면 여러 비동기 작업의 실행 순서를 쉽게 관리할 수 있다는 의미다.
예를 들어, 내가 온라인 쇼핑몰에서 주문을 처리하는 시스템을 개발한다고 가정해보자. 사용자가 주문을 하면, 제품의 재고를 확인하고, 결제를 처리하며, 배송을 준비해야 한다. 이런 각 단계는 서로 다른 시간이 걸릴 수 있으며, 특정 단계가 완료될 때까지 다른 단계를 진행할 수 없는 경우가 있다. 코루틴을 사용하면, 이런 비동기 작업을 효율적으로 관리하여, 각 단계가 필요할 때 정확히 실행되도록 할 수 있다.
suspend 함수
suspend 키워드는 특정 함수가 코루틴 내에서 실행될 수 있음을 나타낸다. suspend 함수는 코루틴 블록 내부나 다른 suspend 함수에서만 호출될 수 있다.
이 키워드는 함수가 비동기 실행을 포함할 수 있으며, 필요한 경우 실행을 일시 중지(suspend)하고 완료될 때까지 기다린 후 다시 실행을 계속할 수 있음을 의미한다. 쉽게 얘기하자면 비동기 작업을 마치 동기 작업처럼 쉽게 다룰 수 있게 해준다는 뜻이다.
예를 들어, 서버에서 데이터를 가져오는 작업을 수행할 때, suspend 함수는 데이터 로딩이 완료될 때까지 실행을 일시 중단하고, 완료되면 자동으로 다시 시작한다. 이러한 방식으로, 코루틴은 개발자가 비동기 코드를 간결하고 이해하기 쉽게 작성할 수 있도록 돕는다.
CoroutineScope
CoroutineScope은 코루틴이 실행될 컨텍스트를 정의한다. 이 스코프는 코루틴이 어떤 스레드에서 실행될지, 어떻게 생명 주기를 관리할지 등을 결정한다. 모든 코루틴은 CoroutineScope 내에서 시작되어야 한다. 이를 통해 개발자는 코루틴의 실행과 취소를 더 세밀하게 제어할 수 있다.
launch
코루틴을 사용하는 기본적인 방법 중 하나는 launch 함수를 사용하는 것이다. launch는 코루틴을 시작하는 가장 일반적인 방법 중 하나로, CoroutineScope의 확장 함수로 제공된다. 이 함수는 코루틴을 시작하고, 작업을 비동기적으로 실행할 수 있게 해 준다.launch를 사용하면 반환 값이 없는 비동기 코드를 실행할 수 있으며, 작업이 완료되기를 기다리지 않고 즉시 다음 코드 라인으로 진행한다.
주의사항
- launch로 시작된 코루틴은 기본적으로 부모 코루틴의 생명주기에 연결된다. 즉, 부모 코루틴이 취소되면 자식 코루틴도 함께 취소된다.
- 코루틴에서 예외가 발생하면, 기본적으로 부모 코루틴에 예외가 전파되어 코루틴의 실행이 중단될 수 있다. 따라서 예외 처리를 적절히 관리하는 것이 중요하다.
- launch는 Job 객체를 반환한다. 이 Job을 사용하여 코루틴의 실행을 제어(예: 취소, 완료 확인 등)할 수 있다.
코루틴을 사용하면 복잡한 비동기 코드를 더 쉽고 직관적으로 작성할 수 있다. launch 함수는 이러한 비동기 작업을 시작하는 데 있어 가장 기본적이면서도 강력한 도구 중 하나이다.
Dispatchers
Dispatchers는 코루틴이 실행될 스레드를 결정하는 역할을 한다. Dispatchers.IO, Dispatchers.Main, Dispatchers.Default 등이 있으며, 각각의 디스패처는 코루틴이 실행될 스레드 풀을 나타낸다.
Dispatchers.IO
I/O 작업에 최적화된 스레드 풀에서 코루틴을 실행한다. I/O 작업의 예시는 다음과 같다:
- 파일 입출력: 로컬 파일 시스템에 데이터를 읽거나 쓰는 작업. 예를 들어, 사용자의 설정을 파일에 저장하거나 파일에서 로드하는 작업.
- 네트워크 통신: 서버에 데이터를 요청하거나 서버로 데이터를 전송하는 작업. REST API 호출, 파일 다운로드, 웹 사이트 크롤링 등이 포함된다.
- 데이터베이스 작업: SQL 쿼리 실행, 데이터 조회 및 업데이트 등 데이터베이스와의 상호작용.
Dispatchers.Main
UI 작업을 처리하기 위한 메인 스레드에서 코루틴을 실행한다(주로 안드로이드 개발에서 사용). UI 작업 예시는 다음과 같다:
- UI 업데이트: 안드로이드에서 UI 컴포넌트(TextView, RecyclerView 등)의 내용을 변경하거나 애니메이션을 실행하는 작업.
- 사용자 이벤트 처리: 사용자의 클릭, 스와이프 등의 입력을 처리하고 응답하는 작업.
Dispatchers.Default
CPU 사용량이 많은 작업에 최적화된 스레드 풀에서 코루틴을 실행한다. CPU 사용량이 많은 작업의 예시는 다음과 같다:
- 대규모 데이터 처리: 배열이나 리스트 등의 큰 데이터 셋에 대한 정렬, 필터링, 맵핑과 같은 연산.
- 이미지 처리: 이미지의 리사이즈, 필터 적용, 변환 작업 등 CPU 연산을 많이 요구하는 작업.
- 복잡한 계산: 과학적 계산, 암호화, 압축 해제 등의 작업이 포함된다.
이러한 구별은 작업을 적절한 스레드에 할당하여 애플리케이션의 반응성과 성능을 최적화하는 데 중요하다.
예를 들어, UI 스레드에서 복잡한 이미지 처리를 수행하면 애플리케이션의 반응성이 떨어질 수 있으므로, 이러한 작업은 Dispatchers.Default를 사용하여 백그라운드에서 실행하는 것이 좋다. 반대로, 사용자에게 실시간으로 반응해야 하는 UI 업데이트는 Dispatchers.Main을 사용해야 한다.
Spring MVC에서 DispatcherServlet과의 차이
Dispatchers와 이름이 유사한 Spring MVC의 DispatcherServlet의 차이점도 알아보자.
DispatcherServlet은 Spring MVC의 핵심 컴포넌트로서, 들어오는 HTTP 요청을 적절한 컨트롤러로 라우팅하고, 처리 결과를 HTTP 응답으로 변환하는 역할을 한다. 이는 MVC(Model-View-Controller) 패턴에서 컨트롤러 역할을 수행하며, 애플리케이션의 프론트 컨트롤러로서 작동한다.
따라서, 코틀린 코루틴의 Dispatchers와 Spring MVC에서의 DispatcherServlet은 이름만 비슷한 서로 다른 개념이다:
- 코틀린 코루틴의 Dispatchers: 코루틴이 실행될 스레드(또는 스레드 풀)를 결정. 비동기 프로그래밍에서 코루틴의 실행 환경을 관리하는 데 사용됨.
- Spring MVC의 DispatcherServlet: HTTP 요청을 처리하고, 그 결과를 HTTP 응답으로 변환하는 데 사용됨. Spring MVC 패턴에서 중앙 집중식으로 요청을 처리하는 역할을 함.
CoroutineScope 사용 예제
Dispatchers.Default
먼저, 간단한 비동기 작업을 수행하는 SampleService 클래스를 가정해보자. 이 서비스는 단순히 문자열 메시지를 로그에 출력하는 기능을 가지고 있다. 그리고 이 작업을 suspend 함수를 사용하여 비동기적으로 수행하고자 한다.
import kotlinx.coroutines.delay
class SampleService {
suspend fun printMessage(message: String) {
delay(1000) // 비동기적으로 1초 대기를 표현
println(message)
}
}
이제 SampleService의 printMessage 함수를 코루틴을 사용하여 호출하는 SampleClass를 만들어 보자. 이 클래스는 CoroutineScope 인터페이스를 구현하여 코루틴을 실행할 수 있는 환경을 제공한다.
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class SampleClass(private val sampleService: SampleService) : CoroutineScope by CoroutineScope(Dispatchers.Default) {
fun triggerAsyncOperation() {
launch {
sampleService.printMessage("Hello, Coroutine!")
}
}
}
SampleClass는 CoroutineScope를 Dispatchers.Default를 사용하여 구현한다. Dispatchers.Default는 CPU 집약적인 작업에 최적화된 스레드 풀에서 실행된다. 여기서는 예제를 단순화하기 위해 사용했지만, 실제 작업의 특성에 맞게 디스패처를 선택해야 한다.
triggerAsyncOperation 함수 내에서 launch 코루틴 빌더를 사용하여 SampleService의 printMessage 함수를 비동기적으로 호출한다. 이는 CoroutineScope에 의해 제공되는 컨텍스트 내에서 실행되므로, SampleClass의 인스턴스가 살아있는 동안에만 작업이 유지된다.
이 예제는 코루틴을 사용하여 비동기 작업을 간단하게 실행하는 방법을 보여준다. CoroutineScope와 launch를 사용하면, 비동기 로직을 쉽게 구현하고 제어할 수 있으며, 코드의 가독성과 유지보수성을 높일 수 있다.
Dispatchers.IO
다음은 CoroutineScope와 Dispatchers.IO를 사용하여 suspend 함수를 비동기적으로 실행하는 예제 코드다:
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
class DynamoDBEventListener(private val dynamoDBService: DynamoDBService) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
fun insertNicknameToDynamoDB(event: FeignNicknameSpringEvent) {
launch {
dynamoDBService.addNicknameToDB(event.nicknameDto.memberId, event.nicknameDto.nickname)
}
}
}
DynamoDBEventListener 클래스의 경우, DynamoDB와의 비동기 통신을 위해 Dispatchers.IO를 사용하고 있다. 이 클래스는 CoroutineScope를 구현하고 있으며, CoroutineScope by CoroutineScope(Dispatchers.IO) 구문을 통해 모든 코루틴 작업을 Dispatchers.IO의 컨텍스트에서 실행하도록 지정한다.
따라서 insertNicknameToDynamoDB 함수 내에서 launch를 사용하여 addNicknameToDB suspend 함수를 호출할 때, 이 함수는 I/O 최적화된 스레드에서 실행되어, 네트워크 I/O 작업을 효과적으로 처리할 수 있다.
🔥 결론
이 방식으로, suspend 함수를 포함한 비동기 로직을 간단하고 효과적으로 관리할 수 있으며, 애플리케이션의 다른 부분에 영향을 주지 않고 비동기 작업을 수행할 수 있다.
코루틴과 suspend 함수를 사용하는 것은 코틀린에서 비동기 프로그래밍을 위한 강력하고 현대적인 접근 방식이다. 적절한 스코프와 디스패처의 선택은 애플리케이션의 특정 요구 사항에 따라 달라질 수 있으며, 이를 통해 개발자는 더 깨끗하고 유지보수하기 쉬운 비동기 코드를 작성할 수 있다.
사이드 팀원인 "개발자의 서랍"님의 블로그도 한번 방문해 보세요! 좋은 글이 많이 있습니다 :)
'프로그래밍 언어 > Kotlin(코틀린)' 카테고리의 다른 글
코틀린과 스프링 부트 3에서 DynamoDB 항목 추가하기 (0) | 2024.02.27 |
---|---|
코틀린과 스프링 부트 3에서 Feign Client 적용하기 (1) | 2024.02.26 |