// 가짜 API 호출
Future<String> fetchUserData() async {
await Future.delayed(Duration(seconds: 2));
return '사용자 데이터';
}
Future<void> loadData() async {
print('데이터 로딩 시작');
String user = await fetchUserData();
print('받은 데이터: $user');
print('데이터 로딩 완료');
}
void main() async {
await loadData();
}
이 코드를 살펴보자, 이 코드는 비동기일까 동기일까?
async / await 키워드가 사용되었으므로 당연히 비동기 동작이다.
하지만 뭔가 이상하다. 이 코드를 dartpad에서 실행시켜보면 아래와 같이 실행된다.
데이터 로딩 시작
받은 데이터: 사용자 데이터 // <--------- 2초 기다렸다가 나타남
데이터 로딩 완료
왜 2초를 기다렸다가 나타나는 것일까?
fetchUserData 메서드의 2초간 Delay 후에 loadData 메서드가 실행되기 때문이다.
여기가 바로 이상한 지점이다.
이건 동기 동작 아닐까? 순차적으로 실행되었으니까 동기라고 볼 수 있지 않을까?
이 의문을 해소하기 위해 비동기 동작을 사용하는 코드의 여정을 따라가야 할 필요가 있다.
1. Callback 지옥
Javascript에서 처음 나온 용어로 비동기 동작을 Javascript에서는 콜백 함수로 처리했다.
그러자 아래와 같이 재밌는 현상이 발생했다.
doSomething(result1 => {
doSomethingElse(result1, result2 => {
doThirdThing(result2, result3 => {
...
});
});
});
서로 의존성 있는 비동기 동작들을 이렇게 처리하려다 보니
가독성이 심각하게 훼손되는 현상이 발생한 것이다.
거기에 더해 리턴값이나 에러값을 주고받기에도 매우 애매해졌다.
가장 안쪽에 있는 함수의 리턴값을 가장 바깥쪽에 전달하려면 모든 함수가 이 값을
주고받아야 하기 때문이다.
2. Promise
콜백 지옥에 대한 대안으로 등장한 것이 Promise다.
Promise 객체를 생성하고
비동기 동작을 이 객체의 멤버 함수인 then 메서드를 통해 체이닝 형식으로 관리한다.
가독성도 잡을 수 있었다.
Promise 객체는 immutable하기 때문에 각 then 메서드가 실행될 때마다 새로운 객체가 반환되고
그때마다 내부 상태가 새로운 에러값 혹은 리턴값으로 바뀌게 되므로 에러 및 리턴값 관리 측면에서도 훨씬 개선되었다.
하지만 Promise 또한 여전히 체이닝 자체가 복잡해질 수 있다거나, 조건문이나 반복문과 함께 쓰기 어렵다는
등의 단점이 있었다.
// 간단한 Promise
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('데이터');
}, 1000);
});
}
// then 체이닝
getData()
.then(data => {
console.log('1단계:', data);
return '처리된 ' + data;
})
.then(result => {
console.log('2단계:', result);
});
3. async / await
드디어 맨 처음 했던 질문에 답할 차례가 되었다.
결론부터 말하자면 async await은 비동기 동작이 맞다.
// 가짜 API 호출
Future<String> fetchUserData() async {
await Future.delayed(Duration(seconds: 2));
return '사용자 데이터';
}
Future<void> loadData() async {
print('데이터 로딩 시작');
String user = await fetchUserData();
print('받은 데이터: $user');
print('데이터 로딩 완료');
}
void main() async {
await loadData();
}
main문에서 await 키워드로 loadData()를 호출하는 부분이 Block되고 loadData가 전부 실행된 후
main문이 끝나기 때문에 마치 동기처럼 보이는 것일 뿐이지
loadData가 실행되고 있는 동안 사용자 입력을 받는 등의 함수들은 여전히 실행되고 있다.
미시적 관점에서만 보면 동기적이지만, 거시적 관점에서 보면 그제서야 실체가 드러나는 것이다.
async / await의 정확한 동작을 아는 것은 매우 중요하다.
서로 연관이 있는 작업들을 async / await을 통해 순차적으로 실행하는 것은 당연히 문제가 없지만,
서로 독립된 작업들을 순차적으로 실행하게 되면 효율성 측면에서 어마어마한 손해를 보기 때문이다.
아래와 같이 서로 연관이 있는 작업들은 async/ await을 통해 작업해도 상관이 없다.
정확히는 오히려 이렇게 처리해야 한다.
각각의 비동기 작업들을
Future<void> dependentTasks() async {
var start = DateTime.now();
String userId = await Future.delayed(Duration(seconds: 1), () => 'user123');
String userPosts = await Future.delayed(Duration(seconds: 1), () => '$userId의 게시글');
String postComments = await Future.delayed(Duration(seconds: 1), () => '$userPosts의 댓글');
}
하지만 서로 다른 100명의 정보를 불러와야 할 필요가 있고 이 작업을 async / await으로 처리한다면?
어마어마한 비효율성이 발생한다.
이렇기 때문에 async / await을 쓰는 상황을 정확히 인지하고 써야 한다.
Future<void> loadUsersWrong() async {
var start = DateTime.now();
List<String> users = [];
for (int i = 1; i <= 100; i++) {
String user = await Future.delayed(Duration(seconds: 1), () => '사용자$i');
users.add(user);
}
}
void main() {
await loadUsersWrong();
}
'CS' 카테고리의 다른 글
배열의 범위를 넘어서면 항상 segfault가 발생할까? (1) | 2025.07.08 |
---|---|
형변환이란? (0) | 2025.05.28 |
운영체제 기본 3 - CPU는 어떻게 컴퓨터를 제어하는가 (0) | 2023.07.08 |
가상화 - 컴퓨팅 자원을 유연하게 쓰는 방법 (0) | 2023.06.29 |
포트와 소켓 - 네트워크 (0) | 2023.06.29 |