![[Develop] 제어 주도에 따른 동기와 비동기](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxH2Si%2FbtsNU5FkZky%2FjK0WOXjWStfzbzfKENhZC0%2Fimg.png)
개요
이전에 Flutter글에서 async/ await 개념에 대해 공부하면서 아래의 글을 작성한 적이 있었다. ▼
[Flutter] 동기와 비동기 개론
개요 동기와 비동기, 프로그래밍을 공부하다보면 항상 등장하는 개념이다. 중요한 개념이고 꼭 알아야한다고 하지만 이게 왜 중요한 개념인지 잘 이해하지 못하고 넘어간 적이 많다.▼ 하지만
noguen.com
해당 글의 내용이 잘못된건 아니지만 (다시 읽어봤을 때는 아직까지는 잘못된 점을 못찾았다) 동기와 비동기를 다르게 보는 시각이 있다는 이야기가 있어서 이에 대해 정리하려고 한다.
제어 흐름으로 본 동기와 비동기
동기와 비동기의 개념
동기는 Synchronous, 비동기는 Asynchronous로 한국어로 봤을 때는 약간 헷갈리는 면도 조금 있다. 동기라는 말 자체가 동시에 라는 의미를 가지고 있기도 하고, 동기화라는 단어에 익숙한 우리는 '동기화 -> 백그라운드에서 실행됨 -> 내가 그동안 다른 것을 할 수 있음' 이라는 인식이 있다. 그렇기에 동시 다발적으로 실행되는 무언가라고 생각이 되지만 오히려 반대로, 동기가 직렬적 실행이고 비동기가 병렬적 실행이다.
'왜 동시에라는 의미가 있는데 직렬적 실행?'
그러면 ...이라는 질문을 하게 된다. 이에 대한 답은 여기서 말하는 동시에가 여러가지 프로세스가 동시에가 아닌 실행과 결과가 동시라는 의미다. 그 말은 즉, 실행과 결과는 하나라는 말로 실행을 하고 결과를 보는 것 까지가 하나의 프로세스라고 정의를 하는 것이다. 당연하게도 비동기는 동시가 아니라는 말이고, 실행을 하고 결과를 보는것을 하나의 프로세스라고 정의하지 않는 것이다.
직렬과 병렬 그리고 제어 흐름 주권
여기까지는 대부분이 알려주지 않아도 아는 내용이라고 생각된다. 동시에 여러가지를 실행하냐 아니냐 정도로 생각하면 쉽게 기억할 수 있다. 그런데 동기와 비동기를 단순히 직렬과 병렬의 개념이 아닌 제어 흐름의 시각으로 볼 수 있다.
'제어의 흐름? 내가 짰는데 제어는 내가 하는거 아냐?'
개발 전체의 관점에서 보면 코드를 짠 사람이 주도권을 가지고 있어야 한다는 생각은 당연하다. 하지만 개발 인원이 많아지고 코드가 복잡해지면 주도권이 나에게 있다고 할 수 없다. 특히 객체지향적 프로그래밍을 하게 되면 제어 흐름의 관점은 개발자가 아니라 프로그램의 객체로 가게 된다.
동기의 제어 흐름 주권
동기의 제어 흐름 주권은 호출자에게 있다. (특정 객체일 수도 있고 프로그램 전체가 될 수 도 있다.) 호출자가 동기 메소드를 호출 혹은 동기 작업을 수행하면 결과가 돌아올 때 까지 주권을 내려놓지 않는다. 즉, 호출과 동시에 Block 상태가 된다.
아래의 코드를 보며 흐름의 주권을 보면 이렇다. ▼
function syncJob(): number {
// 호출자가 여기서 주권을 잡음
const result: number = takeLongTimeJob(); // Block 상태
return result; // 결과까지 모두 호출자 주권
}
`syncJob`이라는 메소드가 호출이 됨과 동시에 호출자가 주권을 잡게 된다. 그리고 해당 메소드의 작업이 끝이 날 때 까지 호출자는 주권을 잡고 있게 되고 프로그램을 블록(Block)하게 된다.
주권이란 사실상 책임과 같다. 그렇기에 이렇게 특정 객체에게 주권이 넘어가게 되면 책임을 물을 대상이 확실해진다. 문제가 생겼을 경우, 즉 버그가 생겼을 경우 어디서 발생했는지를 명확히 알 수 있게 된다. 하지만 책임이 있다고 해서 문제가 해결된다는 것은 아니다. 책임은 있으나 해당 작업이 불가피하게 길어지게 되면, 원인은 명확하지만 해결은 결국 불가능해진다.
즉, 불가피하게 길어지면 원인은 명확하게 파악이 되나 해결인 안된 채로 프로세스가 블록 상태에 빠질 수 있게 된다는 것이다.
비동기의 제어 흐름 주권
비동기의 제어 흐름 주권은 런타임에게 있다. 호출자가 비동기 메소드를 호출 혹은 비동기 작업을 수행하게 되면, 수행과 즉시 바로 런타임 주권이 넘어가게 된다. 그리고 런타임은 이 흐름 주권을 받고 `await` 지점에서 호출자에게 다시 주권을 건네준다.
아래의 코드를 보며 흐름의 주권을 보면 이렇다. ▼
async function asyncJob(): Promise<number> {
// 호출자는 주권을 다시 런타임에게 넘김
const result: number = await takeLongTimeJob();
// 네트워크 응답 시점에서 런타임이 호출자에게 제어권을 다시 돌려줌
return result;
}
`asyncJob`이라는 메소드가 호출이 되고 나서 호출자는 주권을 런타임에게 넘긴다. 그리고 `await`가 걸려있는 `takeLongTimeJob`이 실행될 때 주권을 호출자에게 다시 건네준다. 정확히는 `await`를 만나서 반환된 `Promise`가 `Pending`상태일 때 주권을 다시 호출자에게 넘기게 된다.
비동기는 주권을 계속해서 쥐고 있지 않고 다른 비동기 메소드들이나 스케쥴러에게 넘기게 된다. 다시말해 책임을 오래동안 쥐고 있지 않게 된다.
정리
간단하게 정리를 해보면 아래와 같다. ▼
구분 | 동기 | 비동기 |
주권 이전 | 없음 (호출자가 계속 소유) | 호출 즉시 스케쥴러 혹은 이벤트 루프에게 이전 |
주권 회수 | 함수 리턴 시점에 호출자에게서 회수 | await 이후, Promise(혹은 Future)완료 시점에 다시 호출자에게 회수 |
흐름 추적 | - 단선적(linear) - 비교적 쉬움 |
- 비선형(nonlinear), 콜스택 + 이벤트 큐 기반으로 복합적 - 비교적 어려움 |
동기가 직렬적이라서 흐름 추적이 쉽다라고도 이해할 수 있지만, 주권을 계속 쥐고있고 그 책임을 갖고 있는다는 이해로 흐름 추적이 쉽다고 이해할 수 도 있다. 반대로 비동기는 병렬적이라서 흐름 추적이 어렵다고 생각할 수 있지만 책임을 분산해서 갖고 있기에 특정 문제가 생겼을 때 누구의 책임인지를 묻기가 어려워서 흐름 추적이 어렵다고도 이해할 수 있다.
마치며
이번 글에서는 동기와 비동기를 단순한 ‘순차 vs 병렬’ 관점이 아니라, 제어 흐름의 주권이라는 시각에서 살펴보았다. 글을 대강 요약하자면 이렇다. ▼
- 동기는 호출자가 흐름의 주권과 책임을 끝까지 붙들고 있어, 블록킹이 불가피하지만 문제 발생 시 원인 추적이 명확하다.
- 비동기는 호출 직후 런타임(스케줄러)에 주권을 위임해 더욱 유연한 처리가 가능하지만, 흐름이 분산되면서 책임 소재와 디버깅 난이도가 높아진다.
이처럼 "누가 언제 주권을 넘기고 회수하는지"를 의식하면, 동기와 비동기 메소드의 적절한 사용 시점을 분명히 판단할 수 있다. 앞으로는 실제 애플리케이션 설계 단계에서 주권 이전/회수 타이밍을 명시적으로 다이어그램으로 그려보거나, 프로그램의 흐름 속에서 "이 지점에서 누가 책임을 가져야 할까?"를 계속 질문해 봐야겠다.
'Develop > Develop' 카테고리의 다른 글
[Develop] svg vs png : 뭐가 더 좋을까? (0) | 2025.02.07 |
---|