이전 글 요약
- Docker 도입
- 에러 처리 및 여러 테스트 추가
문제 제기
OnPyRunner01
에서는 “동시 실행이 늘면 코드 실행 끝날 때까지 블로킹” 문제가 있다고 보고,
이를 해결하기 위해 “메시지 큐 기반 비동기 처리"를 도입하려 했다.
하지만 이후 OnPyRunner03 에서는 다시 동기로 전환했는데, 이 과정에서 다음과 같은 개념적 혼동이 있었다.
잘못된 사고 흐름
동시 실행 문제 해결책을 번복했지만 다른 해결책은 제시하지 않음
- 동기 -> 비동기 -> 다시 동기
- 실제 문제(동시 실행) 해결 방법은 논블로킹 구조, 스레드 풀 등 다른 방식이어야 함
블로킹 문제를 비동기로 해결하려고 시도
- 동기/비동기와 블로킹/논블로킹은 별개의 개념
- 비동기라고 해서 블로킹이 자동으로 해결되는 것은 아님
전체 구조가 한 번에 동기/비동기여야 한다는 착각
- “파이썬 코드 실행은 동기로 돌아가니까 전체도 동기”
- 실제로는 jail 내부 실행만 동기이고, 서버 I/O나 요청 처리 등은 별도로 비동기/논블로킹 가능
핵심 개념 정리
- 동기/비동기: 호출자가 결과를 기다리는지 여부
- 블로킹/논블로킹: 실행 중인 스레드나 서버 자원이 막히는지 여부
동기/비동기와 블로킹/논블로킹은 독립적 개념이다.
전체를 동기/비동기 하나로 통일할 필요는 없고 객체마다의 상호작용에 따라 판단해야한다.
OnPyRunner의 최소 MVP는 무엇일까?
스타트업에서는 제품의 가장 중요한 기능에 집중해서 개발하고 초기 모델을 출시하는데 이를 MVP(Minimum Viable Product)라고 한다.
그렇다면 OnPyRunner의 최소 MVP는 무엇일지 고민해 보고 다음과 같이 정리했다.
- Python 코드와 입력이 주어질 때 실행 결과를 반환하는 API
- nsjail 기반의 샌드박스 격리
- Redis 기반의 비동기 작업 처리
Redis의 본질
Redis를 “비동기 처리를 위해 도입하는 도구"라고 생각했지만, OnPyRunner를 구현하면서 느낀 건 **Redis의 핵심 가치는 비동기 자체가 아니라 ‘상태를 외부로 분리하는 것’**에 더 가깝다는 점이었다.
처음에는 “동기로 돌리면 블로킹 문제가 생기니까 -> 비동기로 바꾸자 -> 그러려면 Redis가 필요하다” 라는 단순한 사고 흐름으로 접근했다.
하지만 이건 정확하지 않았다.
OnPyRunner에서 문제의 본질은 비동기 처리 여부가 아니라, 코드 실행이라는 긴 작업의 생명주기가 HTTP 요청의 생명주기에 종속되어 있었다는 점이었다. 이 구조에서는 요청이 끝나는 순간 실행 상태를 관찰하거나 복구할 방법이 없다.
예를 들어 보면 다음과 같다. 사용자가 실행 버튼을 누른 (실행 api를 요청한) 후에 탭을 닫는 상황(api 요청이 끝남)이 발생했다고 가정하자. 이런 상황에서 nsjail 내부에서는 코드가 정상적으로 실행될 수 있다. 하지만 요청이 종료된 시점에서 실행 결과를 수집하고 저장할 책임 주체는 사라진다. 결과적으로 실행은 되었지만, 시스템 입장에서는 해당 실행이 존재한 적이 없는 것과 다름없는 일종의 Orphaned 상태가 된다.
결국 문제의 본질은 실행이라는 작업에 대한 책임이 HTTP 요청에 묶여 있었다는 것이었다. 이는 Job과 Request의 차이를 인지하지 못했기 때문이었다.
Python 코드는 어차피 nsjail 내부에서 동기적으로 실행된다. 이 사실은 바뀌지 않는다.
중요한 건 그 실행을 어떻게 감싸고, 어떻게 관찰하고, 어떻게 결과를 내는가다.
Redis를 도입하면 다음이 가능해진다.
- 실행 요청을 즉시 받아서 큐에 넣고 HTTP 요청은 빠르게 종료
- 실행 상태(PEDDING | RUNNING | COMPLETED)를 Redis에 저장
- 결과 역시 Redis를 통해 조회 가능
- 워커 프로세스 수를 조절해 동시 실행량을 제어
즉, Redis는 “비동기를 하기 위해 억지로 끼워 넣은 컴포넌트"가 아니라 실행이라는 긴 작업을 웹 요청이라는 짧은 생명주기에서 분리하기 위한 장치였다.
Redis를 선택한 이유는 비동기 처리를 위해서가 아니라 실행(Job)의 생명주기를 HTTP 요청으로부터 분리하고, 그 상태를 외부에서 관찰·관리하기 위함 이었다. 이 역할은 Redis가 아니어도 수행할 수 있다. 단지 이런 요구사항을 가장 간단하게 만족시키는 구현 선택지 중 하나일 뿐이다.
그래서 결론적으로는,
OnPyRunner의 API에 Redis 기반의 웹 - 큐 - 워커 구조를 사용하자 라는 판단에 도달했다.