비동기식(Asynchronous) 프로그래밍 구조에서 디버깅 문제 (1)

주싱·2022년 4월 10일
4

Network Programming

목록 보기
10/21

비동기식 구조

A 라는 모듈이 B 모듈에 어떤 서비스를 요청하는 상황을 생각해 보겠습니다. 동기식 프로그래밍 구조에서는 A 는 B 가 요청을 처리하고 응답할 때 까지 선택의 여지 없이 기다려야 합니다. 반면에 비동기식 프로그래밍 구조에서는 A 는 B 가 서비스 처리를 완료할 때 까지 대기하지 않고, 요청만 하고 즉시 다른 일을 할 수 있으며 응답 이벤트가 발생할 때 응답을 처리 할 수 있습니다. 아래 다이어그램을 보시면 이해가 쉽습니다.

장점

비동기식 구조의 장점은 비슷한 요청을 수많은 B 모듈에게 연쇄적으로 요청해야 하는 상황에서 극명하게 드러납니다. 위 그림을 보면 동기식 구조에서는 요청 하나당 응답을 완료한 이후에 다음 요청을 처리하게 되고, 비동기 구조에서는 연쇄적으로 요청을 모두에게 보낸 후에 응답을 대기하기 때문에 처리 속도가 빠릅니다.

단점

비동기적인 실행 흐름을 가지기 위해서는 자연스럽게 A 와 B 는 서로 다른 쓰레드에 의해 처리되어야 합니다. 따라서 문제가 생겼을 때 하나의 흐름을 쭉 따라가며 디버깅할 수가 없습니다. 사람이 생각해야 할 흐름이 몇 개가 더 생기는 것인데 이 흐름이란게 정적이지 않기 때문에 실제 실행해 보지 않고 코드만 보고 문제를 찾기가 동기적인 구조와 비교해서 꽤 어렵습니다.

디버깅 문제

아래는 비동기식 프로그래밍 구조에서 제가 작성했던 버그 2가지가 있는 모듈의 Pseudo 코드입니다.

  • 모듈은 통신 채널로 명령을 전송하고 응답을 대기하는 쓰레드, 그리고 응답을 수신한 후 전송부에 응답 수신을 알리는 두 쓰레드로 구성됩니다.
  • Java, Netty 기반으로 작성되었습니다.
  • 실제 구체적인 코드와는 조금 다르지만 이해를 돕기 위해 간단하게 Pseudo 코드로 재작성해 보았습니다.

여러분 눈에는 문제가 쉽게 보이시나요? 전 코드 한줄 한줄 찍어 보며 디버깅 하기 전에는 도무지 원인을 알 수 없었습니다. 혹시 이 글을 읽으시는 분들에게 생각거리(?)를 드리면 재미있겠다 싶어서 문제로 한 번 남겨두어 봅니다. 이해에 도움이 되시도록 간단한 제약과 배경 설명을 추가하는게 좋겠다는 생각이 듭니다.

  • 모든 자료구조 (Queue, Map 등)는 Thread-Safe 함을 보장한다고 가정하겠습니다.
  • 명령에 대한 응답 ID 는 명령 ID 와 동일합니다.
  • 타겟 시스템은 물리적인 하드웨어 시스템입니다. 그래서 명령을 한 번에 하나만 처리할 수 있는 제약 사항이 있습니다.
  • 그런데 우리 소프트웨어는 비동기적인 구조(Netty)를 가지기 때문에 명령-응답을 동기화 해서 차례로 요청해 주어야 합니다.

송신 코드

  1. Queue 에서 명령 메시지를 꺼냅니다.
  2. 통신 채널로 메시지를 전송합니다.
  3. 명령-응답 동기화 객체를 생성합니다.
  4. 명령 ID 를 키 값으로 동기화 객체를 Map 에 추가합니다. Map 을 사용하는 이유는 명령에 대한 응답을 매칭 시켜 처리해 주기 위해서 입니다. 명령과 관련 없는 응답은 by-pass 됩니다.
  5. 응답 수신을 2초간 기다립니다.
  6. 응답이 없을 경우 예외를 던집니다.
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
	}
}

수신 코드

  1. 채널 수신 Queue 에서 응답 메시지를 꺼냅니다.
  2. Map 에서 응답 ID 에 대한 동기화 객체를 꺼냅니다.
  3. 응답 대기 중인 명령이 있을 때만 처리합니다.
  4. 응답 대기중인 쓰레드에 응답 수신을 알립니다.
  5. 맵에서 응답 객체를 삭제합니다.
  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
}

※ 문제의 힌트는 코드의 위치 그리고 순서와 관련이 있습니다. 예를 들면 송신 코드에서 하던 일을 수신코드에서 했어야 한다거나, 송신 코드의 코드 순서가 이렇게 저렇게 바뀌어야 한다가 되겠습니다.

원인과 해결

비동기식(Asynchronous) 프로그래밍 구조에서의 디버깅 문제 | 해결

profile
소프트웨어 엔지니어, 일상

0개의 댓글