youngseo's TECH blog

[OS] Sync/Async, Blocking/NonBlocking 본문

BackEnd

[OS] Sync/Async, Blocking/NonBlocking

jeonyoungseo 2024. 1. 22. 22:09

개요

이번주 스터디 내용이다! sync/async, blocking/nonblocking은 OS에서도,, 네트워크에서도,, 프로그래밍에서도,, 빈번하게 쓰이는 개념이다. 하지만 이 둘을 절대 같은 의미로 봐서는 안된다 ! 아래는 해당 개념들을 내가 이해한 용어로 설명해보았다.

Sync와 ASync

호출하는 함수가 호출되는 함수의 작업 완료 여부를 신경쓰냐 마느냐가 관심사다.
A가 B를 호출한다. A는 B의 작업이 완료되던 말던 신경 안 쓴다. → Async
반대로, 작업 완료 return문을 기다리거나, 또는 계속해서 (polling 방식처럼) 확인하면서 작업 완료 여부를 신경 쓴다 → Sync

Blocking과 NonBlocking

호출되는 함수가 바로 리턴하느냐 마느냐가 관심사다.
호출된 함수가 ‘나 불렀으니까 이제 너 할 거 해~’라며 호출한 함수에게 제어권을 넘겨주고, 호출한 함수가 다른 일을 할 수 있는 기회를 준다. → NonBlocking
반대로, 호출된 함수가 자신의 작업을 모두 마칠 때까지 호출한 함수에게 제어권을 넘겨주지 않고 대기하게 만든다. → Blocking

위의 그림을 토대로 아래의 글들을 따라가며 이해해보자
 Blocking(바로 리턴 X, 따라서 이 시간동안 호출되는 함수는 다른 일 못함)NonBlocking(바로 리턴 O, 따라서 이 시간동안 호출되는 함수는 다른 일 가능)
Sync(호출되는 함수의 작업 완료를 호출한 함수가 신경 O)우리가 평상시에 많이 보는, 그냥 순차적으로 함수 호출하는 형태라고 보면 된다.

순차적으로 진행되는 함수 A와 B가 있다. 함수 B는 함수 A 가 진행하고 있는 동안 다른 일을 못하고 대기하고 있는다.
함수 A 속에서 함수 B를 불렀다.

A에서 다른 무언가(C)를 하고 있으면서 주기적으로 B가 끝났는지 확인한다. B가 이미 끝났어도 A가 부를 때까지 기다렸다가 return한다. → 이 과정이 polling 형태와 같다.
Async(호출되는 함수의 작업 완료를 호출한 함수가 신경 X)개념적으로 떠올릴 만한 사례가 없다. 그리고 별 다른 장점이 없어서 일부로 사용할 필요가 없다고 한다.

다만, NonBlocking-Async 방식을 쓰는데 그 과정 중에 하나라도 Blocking으로 동작하는 놈이 포함되어 있다면 의도치 않게 Blocking-Async로 동작할 수 있다.
A 속에서 B를 불렀다.

A에서 다른 무언가(C)를 하고 있던지 신경쓰지 않고 B는 자기가 끝나면 return한다.

구현했던 개념 속 해당 내용 살펴보기!

Polling 방식

이전에 Kotlin Spring으로 작업했던 내용 중 @Async 어노테이션을 사용하여 해당 개념을 적용했던 코드가 있어 이를 파악해보았다.

@Async
    fun updateLectureAsync(lecture: Lecture) {

        try {
            // S3 버킷에 저장된 녹음 파일 키
            val audioUrl = lecture.audioUrl
            val key = URI(audioUrl).path.substring(1)

            // S3에서 녹음 파일 내려받기
            val s3Object = s3Client.getObject(GetObjectRequest(bucketName, key))
            val audioBytes = s3Object.objectContent.readAllBytes()

            // Whisper API 호출하여 TTS script 생성하기
            val model = "whisper-1" // Whisper 모델명
            val request = WhisperDto.Request(model, audioBytes)
            val script = whisperService.transcribeAudio(request).take(2300)

            if (script.isEmpty()) {
                lecture.status = Lecture.Status.STT_EMPTY
                return
            }

            // GPT API 호출하여 script를 기반으로 Lecture 생성하기
            val generatedText = gptService.completeChat(script)
            val parsed = objectMapper.readValue<GptDto.LectureResponseDto>(generatedText)
            lecture.score = parsed.score
            lecture.strength = parsed.strength
            lecture.weakness = parsed.weakness
            lecture.transcribed = script
            lecture.status = Lecture.Status.SUCCESS
        } catch (e: Exception) {
            lecture.status = Lecture.Status.FAILURE
        } finally {
            lectureRepository.save(lecture)
        }
    }

1. client에서 updateLectureAsync 함수를 부르는 과정(polling) - Sync&NonBlocking

위 코드는 client에서 서버로 계속해서 updateLectureAsync 함수를 polling 방식으로 부른다. 즉 1분에 한 번씩 updateLectureAsync 함수가 있는 API를 부르며 chatGPT API 결과값 나왔니?를 확인하는 과정이다. client와 서버 간 통신은 기본적으로 stateless한 HTTP 통신이기 때문에 polling 방식을 사용하게 되면 웹브라우저와 서버 간 통신을 한 번 끊어줄 수 있다는 장점이 있다.
암튼, 이 polling 방식을 사용함으로써 client 상에서는 해당 함수를 호출해두고 다른 api를 사용할 수도 있다. 하지만 polling 방식이기 때문에 1분에 한 번씩 호출되는 함수의 작업 완료 여부를 확인하므로 Sync-NonBlocking 방식이라고 할 수 있다.

2. 위 updateLectureAsync 함수 내에서 GPT API를 호출하는 과정 - Sync&Blocking

updateLectureAsync 함수는 GPT API 호출 동안 다른 일을 하지 않고 대기하며, GPT API 작업이 완료되면, 완료된 리턴값을 이용해 Lecture.Status를 SUCCESS로 할당한다. 따라서 아래 함수에서 GPT API를 호출하는 과정은 Sync-Blocking이다.


마무리

비동기-논블로킹 방식 통신으로 요즘 가장 떠오르고 있는 것은 단연 Redis나 Kafka 와 같은 Pub-sub 구조일 것이다. 내가 구현했던 사내 프로세스 중에, 여러 유저들의 요청값들이 동시에 많이 들어올 경우 429로 차단하고 있는 아키텍처가 있는데, 꼭 개선해보고 싶었다..! 다음번에는 async-nonblocking에 대한 이해도를 바탕으로 pub-sub 구조의 아키텍처로 개선해보면 좋겠다. ✨