A 라는 모듈이 B 모듈에 어떤 서비스를 요청하는 상황을 생각해 보겠습니다. 동기식 프로그래밍 구조에서는 A 는 B 가 요청을 처리하고 응답할 때 까지 선택의 여지 없이 기다려야 합니다. 반면에 비동기식 프로그래밍 구조에서는 A 는 B 가 서비스 처리를 완료할 때 까지 대기하지 않고, 요청만 하고 즉시 다른 일을 할 수 있으며 응답 이벤트가 발생할 때 응답을 처리 할 수 있습니다. 아래 다이어그램을 보시면 이해가 쉽습니다.
비동기식 구조의 장점은 비슷한 요청을 수많은 B 모듈에게 연쇄적으로 요청해야 하는 상황에서 극명하게 드러납니다. 위 그림을 보면 동기식 구조에서는 요청 하나당 응답을 완료한 이후에 다음 요청을 처리하게 되고, 비동기 구조에서는 연쇄적으로 요청을 모두에게 보낸 후에 응답을 대기하기 때문에 처리 속도가 빠릅니다.
비동기적인 실행 흐름을 가지기 위해서는 자연스럽게 A 와 B 는 서로 다른 쓰레드에 의해 처리되어야 합니다. 따라서 문제가 생겼을 때 하나의 흐름을 쭉 따라가며 디버깅할 수가 없습니다. 사람이 생각해야 할 흐름이 몇 개가 더 생기는 것인데 이 흐름이란게 정적이지 않기 때문에 실제 실행해 보지 않고 코드만 보고 문제를 찾기가 동기적인 구조와 비교해서 꽤 어렵습니다.
아래는 비동기식 프로그래밍 구조에서 제가 작성했던 버그 2가지가 있는 모듈의 Pseudo 코드입니다.
여러분 눈에는 문제가 쉽게 보이시나요? 전 코드 한줄 한줄 찍어 보며 디버깅 하기 전에는 도무지 원인을 알 수 없었습니다. 혹시 이 글을 읽으시는 분들에게 생각거리(?)를 드리면 재미있겠다 싶어서 문제로 한 번 남겨두어 봅니다. 이해에 도움이 되시도록 간단한 제약과 배경 설명을 추가하는게 좋겠다는 생각이 듭니다.
loop {
Message command = commandQueue.pop(); // 1
channel.write(command); // 2
SyncObject syncObj = new SyncObject(); // 3
syncMap.put(command.id, syncObj); // 4
if(!syncObj.wait(2000)) { // 5
throw new Exception("no response"); // 6
}
}
loop {
Message response= responseQueue.pop(); // 1
syncObj = syncMap.get(response.id); // 2
if (syncObj != null) { // 3
syncObj.signal(); // 4
syncMap.remove(response.id); // 5
}
pipeline.next(response); // 6
}
※ 문제의 힌트는 코드의 위치 그리고 순서와 관련이 있습니다. 예를 들면 송신 코드에서 하던 일을 수신코드에서 했어야 한다거나, 송신 코드의 코드 순서가 이렇게 저렇게 바뀌어야 한다가 되겠습니다.