개요
이번주 스터디 내용이다! 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 구조의 아키텍처로 개선해보면 좋겠다. ✨
'BackEnd' 카테고리의 다른 글
[Java|Spring] 동시성 문제 (0) | 2024.07.21 |
---|---|
[Redis|Flask] 트래픽 제어를 위한 Redis (2) | 2024.01.27 |
[DB] @Transactional 이해하고 사용하기 (0) | 2024.01.17 |
[Java|Spring|JSP] Koala 출석부 제작 후기 ..그리고 보완 방향! (0) | 2024.01.16 |
[DB] RDS DB 마이그레이션 과정 - RDB와 NoSQL (0) | 2024.01.06 |