앱을 만들다보면 비동기 처리를 필수적으로 할 때가 반드시 오곤 합니다. 특히 네트워크나 db 접근과 관련된 일을 하기 시작하면 원하지 않아도 할 수 밖에 없게 됩니다. 그렇다면 비동기가 무엇인지, 그리고 동기가 무엇인지, 안드로이드에서는 비동기 처리를 어떻게 할 수 있는지 알아봐야겠죠? 시작해봅시다.
동기 vs 비동기
동기 (Synchronous : 동시적으로 발생하는)
일단 문자 그대로는 동시적으로 발생하는 것이 동기입니다. 그럼 뭐가 동시에 발생한다는 것일까요? 만약에 우리가 계산기에 1 + 1 이라고 입력했을 때 어떤 일이 일어날까요? 당연히 2 가 출력이 될 것입니다. 어떤 수식을 입력해도 그에 대한 결과가 지체없이 바로 출력이 되죠. 이렇게 요청과 결과가 동시에 일어나는 것을 동기라고 합니다. 계산기 역시도 어떤 알고리즘에 의해 구현된 프로그램인데, 어떤 값을 입력하면 계산기의 로직이 실행되고 로직이 모두 실행된 후에 결과가 반드시 출력이 되어야 합니다.
여기서 중요한 것은, 어떤 요청이 발생하면 다른 작업보다도 먼저 요청에 대한 결과가 먼저 출력이 되어야 한다는 것입니다. 굉장히 당연한 소리 같아보이는 것은 우리는 보통 비동기보다는 동기적으로 프로그래밍을 하기 때문에 그렇습니다. 요청과 결과가 동시에 발생하는 코드에 익숙한 것이죠. 어쨌든 동기적으로 돌아간다고 한다면 먼저 들어온 요청에 의한 로직이 돌고 있는 동안은 다른 작업은 대기하는, 즉 block 상태가 됩니다.
조금 다른 이야기인데, 네트워크 중 TCP 프로토콜에는 3-way handshake라는 개념이 존재합니다. 클라이언트와 서버가 서로 데이터를 주고 받기 전에 세팅하는 사전 준비와 같은 것인데, 서로에게 응답 신호를 보냄으로써 연결 설정을 하는 것입니다. 네트워크 주제가 아니므로 자세한건 생략하고 요점은 Client -> Client의 SYN 요청에 대한 Server의 응답 (ACK) -> Server의 ACK에 대한 Client의 ACK 순으로 요청과 결과가 순차적으로 발생합니다. 이 3-way handshake가 이루어지는 동안에는 다른 작업이 끼어들 자리 따윈 존재하지 않습니다. 즉, 다른 작업은 block 상태가 되어 이 과정이 끝나기를 기다리게 되죠. (물론 네트워크 설정이 저게 끝은 아니라 더 기다릴 수 있습니다.)
비동기 (Asynchronous : 동시에 일어나지 않는)
위에서 계속 이야기한 동기는 의식이 흐르는대로 코드를 작성할 수 있기 때문에 구현하기는 쉽지만 단점이 있습니다. 특정 요청에 의한 코드 블록이 실행되는 동안 다른 코드는 대기를 하는 block 상태에 빠진다는 것입니다.
애플리케이션, 그 중에서 게임의 경우를 생각해 봅시다. 게임을 하기 위해서 '게임 방 입장' 버튼을 터치하면 디바이스는 서버와 네트워크 통신을 시작할 것입니다. 그런데 만약 이런 네트워크 연결 작업을 메인 스레드 (UI 스레드라고도 부르죠.) 에서 처리한다고 가정합시다. 메인 스레드는 UI를 그리는 역할을 하는데 네트워크 연결과 같이 부하가 큰 작업을 하게 될 경우 UI를 그리지 못하고 대기하게 됩니다. 말 그대로 아무런 UI를 그리지 못합니다. '네트워크 연결 중입니다. 잠시만 기다려주세요..' 라던가 뱅글뱅글 돌아가는 로딩 이미지도 띄우지 못한다는 것이죠. 그럼 어떻게 될까요? 디바이스가 완전히 멈춘 것처럼 보입니다. 사용자는 앱이 중단된 줄 알고 강제종료를 하려고 할 것이며, 실제로 이런 현상을 ANR (Application Not Responding, 응답 없음) 이라고 부릅니다.
그렇기 때문에 비동기라는 개념이 중요한 것입니다. 만약 메인 스레드는 그대로 UI를 그리는 작업만을 진행한다고 하고, 네트워크 연결은 별도의 스레드를 만들어서 따로 작업을 진행하면 어떨까요? 앱은 멈춘 것처럼 보이지 않을테고, (적어도 로딩 중이라는 애니메이션을 보여줄 수 있을테니까요!) 네트워크는 네트워크대로 연결 작업을 진행하게 될 것입니다. 이처럼 여러 작업을 각자의 스레드 (또는 프로세스, 또는 기타 등등으로) 가 나누어 가져가서 여러 작업을 block 없이 진행할 수 있도록 하는 것을 비동기라고 합니다.
스레드는 자신이 맡은 작업을 동기적으로 실행하게 될 것이며, 이런 스레드가 여러개 모여 다중 작업을 대기 없이 실행할 수 있게 됩니다. 중요한 것은 '다른 스레드와 관계 없이 자신의 작업을 대기 없이 할 수 있다는 것'이며 이러한 것을 non-blocking 이라고 합니다. 각 스레드는 작업이 언제 시작하든지 다른 스레드의 눈치를 볼 필요가 없고 끝나는 시점도 크게 상관이 없어집니다.
하지만 이전 포스팅에도 있었지만 멀티 스레드의 단점이 있었죠? 구현하기도 쉽지 않을 뿐만 아니라 데이터의 동기화를 위한 처리를 해주어야 한다는 것입니다.
안드로이드의 Coroutine
코루틴 (Coroutine) 이란?
안드로이드 JetPack에는
코루틴이라는 것이 존재합니다. 비동기 작업을 최소화하기 위해 구현된 것으로 Kotlin을 사용한다면 권장하는 비동기 처리 방식이라고 합니다. 공식 문서에 따르면 아래와 같은 특징을 가지고 있습니다.
조금 어려워보이는데, 핵심 내용만 쉽게 설명하자면
- 하나의 스레드에 여러 개의 코루틴을 사용할 수 있습니다. 즉 하나의 스레드에서도 여러 개의 비동기 작업을 block 없이 진행할 수 있습니다. 대신 동기화 작업은 잘 해야겠죠?
- 비동기 작업을 하기 위한 코드가 굉장히 단순합니다. 아래의 코드를 보면 알 수 있습니다. 진짜로 저게 끝입니다. Scope를 생성해서 어떤 스레드에서 돌릴지 결정하고 (IO 스레드, Main 스레드 등..) 작업을 위한 메서드는 suspend 키워드를 붙여 사용하면 됩니다. 물론 복잡한 기능을 만들기 시작하면 코루틴의 여러 기능을 이용하기 시작해야하지만요.
fun main() {
CoroutineScope(Dispatchers.IO).launch {
...
}
}
suspend fun job() {
...
}
- 코루틴을 쉽게 중지할 수 있습니다. cancel()을 이용하면 명시적으로 중지할 수 있고, 주어진 작업이 다 끝나면 알아서 끝나기도 합니다.
- viewModel, WorkManager 등 안드로이드 JetPack과 매우 큰 호환성을 가지고 있기 때문에 비동기 작업을 디자인 패턴, 백그라운드 작업에 맞춰서 쉽게 구현할 수 있습니다.
비동기 작업을 쉽게 처리해주는 것이라고 했지만, 물론 코루틴을 공부해야 가능한 일이겠죠. 아래의 링크를 통해 코루틴이 무엇인지, 어떻게 사용하는 것인지 학습해보세요. 해당 포스팅에 담기에는 내용이 방대한 것 같습니다. CoroutineScope, withContext, Dispatchers.IO, launch, async, deffered 등이 무엇인지 찾아보세요. 하나씩 찾아보다보면 조금씩 감을 잡으실 것입니다.
마치며..
사실 동기와 비동기에 대한 내용만 적으려고 했는데, 어쩌다보니 코루틴을 간단하게 소개하게 되었네요. 그런 것치곤 너무 내용을 담지 않아서 괜히 적었나 싶기도 하고.. 그냥 안드로이드에는 비동기를 처리하기 위해 도움을 주는 여러가지 것들이 존재한다는 것을 알아주셨으면 합니다.
원래는 AsyncTask라는 것이 존재했는데, 현재는 deprecated 되었습니다. 이건 저도 사용해본 적이 없어서 잘 모르지만, 화면 회전과 같이 계속 생성이 발생함에 따라 Async 자체도 계속적으로 쌓이게 되어 메모리 누수가 크고, 예외 처리를 하기 위한 작업도 따로 구현해야하고, 속도도 느리고... 여러 이유가 있었다고 합니다. 상당히 무거운 클래스였나 봅니다. 기회가 된다면 한번 사용해 보시는 것도 좋을 것 같네요.
오류나 피드백은 언제나 환영입니다. 그럼 오늘도 읽어주셔서 감사합니다!
댓글
댓글 쓰기