EDITORCLASSBLOGLOGINimg default descriptionimg default descriptionimg default description표현한다는 것의무한한 가능성,새로운 형태로
담아내다.
img default descriptionimg default descriptionimg default descriptionimg default descriptionimg default descriptionimg default descriptionimg default description새로운 형태의 콘텐츠Opkle은 코드 없이 웹을 마음껏 만들고, 누구나 자기 화면을 그릴 수 있도록 에디터를 만들고, 그 결과물을 어디서나 즐길 수 있게 돕는 팀입니다.텍스트와 화면과의 조화를 통해, 웹을 짓는다는 것이 그저 단순한 코딩이 아닌, 상상을 펼치고 감각을 깨우는 과정이 될 수 있도록 좋은 도구를 만들어 냅니다.
옵클 에디터 개발기: Valkey 큐 서버의 기본 구조dev7옵클 에디터 개발기: Worker loop와 AI 결과 검증dev8옵클 에디터 개발기: 미리보기가 그대로 코드로dev9옵클 에디터 개발기: 그래픽 에디터도 결국 직접 만들기로 했다dev10옵클 에디터 개발기: Rust 공장을 그래픽 엔진으로 다듬다dev117891011...7 옵클 에디터 개발기: Valkey 큐 서버의 기본 구조책을 만들기 위한 LLM 구조를 잡고 나니, 다음 단계는 모델 호출 자체가 아니었습니다. 실제 제품 안에서 AI 기능을 돌리려면 사용자의 요청이 안정적으로 들어가고, 작업 종류에 맞는 실행 경로로 분기되고, 중간에 worker가 재시작되어도 요청이 사라지지 않아야 합니다. 그리고 결과는 다시 에디터가 이해할 수 있는 형태로 돌아와야 합니다. 이 흐름이 없으면 아무리 좋은 모델을 붙여도 사용자는 버튼을 누른 뒤 불안정한 대기 상태를 경험하게 됩니다.옵클 에디터의 AI 기능은 처음부터 짧은 텍스트 요청 하나로 끝나는 구조가 아니었습니다. 문장 단위 변환처럼 상대적으로 가벼운 작업이 있고, 선택 구간의 구조를 고치는 작업도 있습니다. 자연어를 편집 가능한 명령으로 바꾸는 작업도 있고, 이미지에서 구조를 읽어 내는 작업도 있습니다. 여기에 생성 시간이 긴 미디어 작업까지 붙으면, 대기 시간과 실패 양상이 완전히 달라집니다.그래서 저는 큐 서버를 단순한 background worker로 만들지 않았습니다. 이 서버는 AI 요청을 “나중에 처리하는 곳”이 아니라, 이질적인 작업을 하나의 runtime 위에서 조율하는 계층입니다. 요청을 타입으로 나누고, 같은 작업 경로에서는 최신 의도만 남기며, 공유 저장소 위에서 용량과 배타성을 관리한 뒤, 결과를 요청 단위로 다시 돌려줍니다. 에디터 입장에서는 AI가 하나의 기능처럼 보이지만, 뒤에서는 서로 다른 비용 함수와 실패 모드를 가진 작업들이 따로 움직입니다.기본 저장 축은 빠른 상태 저장소와 오래 남는 기록 저장소로 나누었습니다. 빠른 쪽은 사용자가 기다리는 작업 상태와 결과 반환에 맞습니다. 느린 쪽은 어떤 요청이 어떤 결과로 끝났는지 추적하는 쪽에 가깝습니다. 사용자는 빠르게 결과를 받을 수 있고, 서버는 완료된 작업의 궤적을 남길 수 있습니다.Typed work domains가장 먼저 나눈 것은 작업의 type space였습니다. 모든 AI 요청을 하나의 범용 입력으로 합치면 처음에는 편합니다. 하지만 입력 공간과 출력 공간이 다른 작업을 같은 식으로 다루면, 나중에 어떤 불변식이 깨졌는지조차 말하기 어려워집니다. 그래서 저는 요청을 “문장을 바꾸는 일”, “편집 명령을 만드는 일”, “시각 구조를 추출하는 일”, “생성 시간이 긴 미디어를 다루는 일”처럼 서로 다른 도메인으로 분리했습니다.각 도메인에는 요청 단위 식별자와 경로 단위 식별자가 필요합니다. 요청 단위는 사용자가 기다리는 한 번의 대기에 대응하고, 경로 단위는 같은 UI 자리에서 반복되는 의도를 묶습니다. 같은 자리에서 짧은 시간 안에 여러 번 누르면 요청 식별자는 늘어나지만, 경로 식별자는 같습니다. 이 둘을 구분하지 않으면 시스템은 “몇 번 눌렸는지”와 “지금 무엇을 원하는지”를 동시에 표현할 수 없습니다.이 구분은 중요했습니다. 모든 과거 요청을 끝까지 처리하면, 늦게 도착한 오래된 결과가 최신 UI 상태를 덮을 수 있습니다. 반대로 과거 요청을 조용히 사라지게만 하면, 그 요청을 기다리던 화면은 영원히 pending에 남습니다. 그래서 요청 단위는 결과 반환의 단위로 보존하되, 경로 단위는 최신 의도만 고르는 기준으로 사용했습니다. 큐잉 이론으로 말하면, 같은 클래스 안의 도착 사건들을 coalesce 하면서도, 각 도착에 대한 종료 신호는 남기는 설계입니다.타입을 나누면 코드가 길어집니다. 하지만 AI 기능은 코드가 짧은 것보다 계약이 명확한 것이 중요합니다. 범용 큐로 시작하면 처음에는 편하지만, 나중에는 어떤 작업이 어떤 입력을 필요로 하는지, 어떤 실패가 재시도 가능한지 알기 어려워집니다. 작업 종류별 failure mode가 다르기 때문에, 결과 공간도 처음부터 다르게 잡는 편이 맞았습니다.Output invariants각 작업 도메인은 단순 옵션이 아니라 output invariant를 가집니다. 어떤 작업은 입력과 출력의 구조적 대응을 깨면 안 됩니다. 문단 경계를 유지하면 에디터가 결과를 다시 문서에 붙일 수 없고, 문체를 바꾸면 작가의 의가 무너집니다. 어떤 작업은 산문이 아니라 기계가 적용 가능한 구조만 허용해야 합니다. 자연어 응답이 아무리 그럴듯해도, 에디터가 해석할 수 없으면 그 결과는 실패한 것입니다.여기서 말하는 계약은 “잘 써 주세요” 수준의 문장이 아닙니다. 입력에서 출력으로 가는 함수가 보존해야 할 성질입니다. 구조 보존형 변환은 분할 단위의 개수와 순서를 지켜야 합니다. 명령 추출형 변환은 허용된 명령 집합 안의 원소만 반환해야 합니다. 추출형 변환은 스키마를 만족하는 구조만 유효합니다. 생성 시간이 긴 작업은 임시 참조가 아니라, 제품이 이후에 다시 읽을 수 있는 결과로 끝나야 합니다.같은 텍스트 처리 경로 안에도 성격이 다른 모드가 있습니다. 어떤 모드는 본문을 고치는 쪽에 가깝고, 어떤 모드는 짧은 명령을 해석하는 쪽에 가깝습니다. 겉으로는 비슷한 AI 버튼처럼 보여도, 모델에게 허용하는 자유도와 검증 기준이 다릅니다. 이 차이를 queue 계층에서 타입으로 고정해 두지 않으면, worker는 모든 요청을 같은 생성 문제로 취급하게 됩니다.이 invariant를 요청 시점에 붙여 두는 이유는 retry와 validation 기준을 다르게 쓰기 위해서입니다. 스키마만 맞으면 되는 작업과, 구조적 대응까지 맞아야 하는 작업은 같은 “성공” 정의를 가질 수 없습니다. 큐 서버는 모델을 호출하기 전에, 이번 요청이 어떤 성공 조건을 갖는 문제인지부터 확정해야 합니다.Latest-intent coalescing큐 서버의 핵심은 모든 요청을 하나의 전역 순서만으로 처리하지 않는다는 점이었습니다. 전역 대기열은 worker를 깨우는 신호에 가깝고, 실제 최신 의는 경로별로 따로 유지됩니다. 사용자가 요청을 넣으면 시스템은 그 경로의 현재 의를 갱신하고, worker는 나중에 그 경로의 최신 값만 집어 갑니다.이 구조의 수학적 직관은 단순합니다. 같은 경로에서 시간순으로 들어온 요청 수열 \(r_1, r_2, \ldots, r_n\)이 있을 때, 실행 대상은 최댓값에 해당하는 \(r_n\)이고, 그보다 앞선 원소들은 더 이상 유효한 해가 아닙니다. 다만 앞선 원소들을 기다리는 클라이언트가 있을 수 있으므로, 그 요청들은 “무시됨”이 아니라 “종료됨”으로 닫혀야 합니다. 최신성 보존과 대기 상태의 유한성은 함께 만족되어야 하는 조건입니다.그래서 오래된 요청을 그냥 버리지 않습니다. 최신 target이 아닌 같은 그룹의 요청들은 취소된 결과로 반환합니다. 취소도 결과입니다. 프론트가 영원히 pending을 붙들고 있지 않으려면, 시스템이 각 요청 식별자에 대해 명확한 종료 상태를 보장해야 합니다.이 coalescing은 사용자 경험과 비용 제어를 동시에 해결합니다. 사용자가 같은 기능을 여러 번 눌렀을 때, 서버는 가장 최신 의도만 실행하고 이전 의는 닫습니다. 비용이 큰 작업일수록 의미 없는 중복 실행을 줄이는 효과가 커집니다. AI 기능이 제품 안에서 안정적으로 보이려면, 이런 오래된 요청 정리 흐름이 반드시 필요했습니다.경로 식별자는 보호 경계로도 쓰입니다. 작업 종류마다 허용되는 경로 규칙이 있고, 규칙 밖의 요청은 실행 계층까지 내려가지 않습니다. 프론트나 라우터의 실수가 외부 호출과 비용 발생으로 이어지지 않게 하는, 큐 계층의 사전 검증입니다.Atomic state transitions공유 저장소 위에 큐를 만들 때 가장 신경 쓴 부분은 atomic boundary였습니다. 경로의 최신 의를 갱신하고, worker에게 일이 생겼음을 알리는 동작은 겉으로 보면 단순합니다. 하지만 이 갱신이 여러 단계로 쪼개지면 중간 상태가 생깁니다. worker가 그 사이에 읽거나, 같은 경로에 새 요청이 다시 들어오면 최신성 보장이 흔들릴 수 있습니다.그래서 enqueue와 requeue는 하나의 원자적 전이로 묶었습니다. 관측 가능한 상태는 “갱신 전”과 “갱신 후”만 있어야 하고, 반만 바뀐 상태는 없어야 합니다. 분산 시스템에서 말하는 linearizability를 큐 갱신에 작게 적용한 셈입니다. worker는 중간 상태를 보지 않고, 경로별 최신 의도만 읽으면 됩니다.slot 확보도 같은 이유로 원자적이어야 합니다. 저는 작업 종류별로 유한한 용량 집합을 두었습니다. 용량을 잡을 때는 먼저 만료된 점유를 회수하고, 남은 자리가 있을 때만 새 점유를 기록합니다. 이 판정과 기록이 한 번에 끝나지 않으면, 두 worker가 같은 빈자리를 동시에 봤다고 착각할 수 있습니다.이 방식은 고정 세마포어와 비슷하지만, 각 점유가 만료 시각을 갖는 lease에 가깝습니다. worker가 비정상 종료되어 반납을 못 하더라도, 시간이 지나면 다음 확보 과정에서 자리가 회수됩니다. 짧은 작업과 긴 작업을 같은 용량 풀에 넣지 않는 이유도 여기에 있습니다. 한 종류의 대기가 다른 종류의 응답성을 잠식하지 않게 하려면, 자원 공간을 처음부터 분할해야 합니다.Capacity partitions작업 종류별 용량 한도는 큐 서버의 성격을 결정합니다. 어떤 작업은 비교적 빠르고 파싱이 중심입니다. 어떤 작업은 외부 생성과 결과 정리가 더 깁니다. 어떤 작업은 비동기 완료를 오래 기다려야 합니다. 그래서 이 셋을 같은 concurrency로 묶으면 리소스 배분이 어긋납니다.다계급 큐잉(multi-class queueing)의 관점에서 보면, 이는 서로 다른 서비스 시간 분포를 가진 클래스에 별도의 서버 풀을 주는 설계입니다. 짧은 작업 클래스는 더 많은 동시성을 가질 수 있고, 긴 작업 클래스는 더 적은 동시성과 더 긴 lease를 가집니다. 한도를 다르게 두지 않으면, 긴 작업이 짧은 작업의 대기열을 잠식하거나, 짧은 작업의 과다 점유가 긴 작업의 안정성을 해칩니다.용량 lease와 경로 배타성의 수명도 같지 않습니다. 용량은 실제 실행 시간을 기준으로 잡고, 경로 배타성은 같은 의가 너무 오래 묶이지 않도록 잡는 기준입니다. 이렇게 세분화하지 않으면 짧은 작업은 과하게 오래 막히고, 긴 작업은 중간에 보호가 풀려 중복 실행될 수 있습니다.용량을 못 잡았을 때는 바로 실패시키지 않습니다. 최신 의는 다시 대기열로 돌아가고, 더 이상 유효하지 않은 과거 요청만 닫습니다. 용량 부족은 일시적인 backpressure이기 때문입니다. 큐 서버는 이 상태를 오류로 보지 않고, 다음 순환에서 다시 시도하도록 합니다. 사용자의 최신 요청은 보존하고, 오래된 요청은 종료합니다.이 정책을 두면 큐 서버는 단순 대기열이 아니라 리소스 스케줄러가 됩니다. 텍스트와 미디어는 같은 AI 기능이라는 이름 아래 있지만, 서버 입장에서는 완전히 다른 workload입니다. 용량 분할은 이 workload들을 서로 방해하지 않게 나누는 장치입니다.Lease and exclusion용량을 잡았다고 해서 곧바로 외부 작업을 시작하면 안 됩니다. 같은 경로가 이미 처리 중인지 확인해야 합니다. 그래서 실행 단위 registry와 경로 단위 registry를 따로 두었습니다. 실행 단위는 지금 worker가 실제로 수행 중인 작업의 집합입니다. 경로 단위는 같은 의도 경로에서 두 실행이 겹치는 것을 막습니다.실행 단위 registry는 프로세스 재시작 판단에도 쓰입니다. in-flight 작업이 없으면 안전하게 끊을 수 있고, 하나라도 있으면 보류하는 편이 낫습니다. 아직 집어 가지 않은 작업은 대기열에 남아 있으므로 재시작 후 다시 처리할 수 있습니다. 실제 위험한 것은 이미 집어 가 외부 효과를 만들고 있는 작업입니다.경로 단위 registry는 mutual exclusion에 가깝습니다. 같은 경로에 유효한 lease가 남아 있으면 새 실행은 들어가지 못합니다. 이때 이미 잡은 용량은 반납하고, 최신 의도만 다시 대기열로 올립니다. 같은 문단이나 같은 미디어 자리에서 두 worker가 동시에 결과를 만들지 않게 하는 장치입니다.stale 판정은 보수적으로 잡았습니다. 정상 진행 중인 작업을 죽은 것으로 오판하는 비용이, 잠깐 더 기다리는 비용보다 크기 때문입니다. 동시에 무한히 막히지 않도록, 시간이 지난 lease는 회수하는 sweep도 둡니다. 비정상 종료로 registry가 남아 있더라도 시스템은 결국 수렴해야 합니다.경로 lock은 또 다른 축입니다. 용량 제한, 경로 배타성, 경로 처리 경계는 비슷해 보이지만 역할이 다릅니다. 용량은 작업 종류별 동시성 한도이고, 경로 배타성은 같은 의의 중복 실행 방지이며, 경로 lock은 그 경로를 처리하는 구간의 경계입니다. 이 셋을 분리했기 때문에 실패 상황에서 어떤 상태를 풀어야 하는지 명확해졌습니다.이 단계에서 큐 서버의 기본 구조는 정리되었습니다. 타입을 나눈 작업 공간, 출력 불변식, 최신 의도 coalescing, 원자적 상태 전이, 용량 분할, lease 기반 배타성이 하나의 실행 모델로 맞물렸습니다. 사용자는 버튼 하나를 누르지만, 서버는 그 요청을 경로 단위로 최신화하고, 작업 종류별 용량에 태우고, in-flight 상태를 기록하고, 오래된 요청을 닫아 줍니다. 반복 테스트에서도 최신 의도 우선 처리, 중복 실행 방지, backpressure, restart 판단, stale lease 회수가 안정적으로 통과했습니다.이전글목록으로다음글저작권 고시Copyright Notice본 웹사이트의 모든 디자인 결과물 및 영상에 대한 저작권은 Abstract Cloud에 있으며, 저작권법 및 관련 법령에 의해 보호받습니다. 웹, 영상, 본문, 표지, 내지 디자인을 포함한 모든 콘텐츠는 저작권자의 자산으로, 사전 동의 없이 무단 복제, 배포, 2차 저작물 제작, 온라인 공유 등을 금지합니다. 이를 위반할 시, 저작권법에 따라 민형사상 책임을 질 수 있습니다. 정당한 구매와 저작권 보호는 창작자의 권리를 지키며, 더 나은 작품으로 보답할 힘이 됩니다.저작권자: Abstract Cloud | 대표자: 배창규(uragen)
© Abstract Cloud. All Rights Reserved.
HOMEFAQ이용 약관개인정보 이용방침help@opkle.app
010-2747-3403
상호 :추상적 형상 디자인(Abstract cloud)  |  대표자 :배창규사업자등록번호 :249-74-00533통신판매업 신고번호 :2025-의정부송산-0634주소 :경기도 의정부시 부용로 49, 108동 402호웹의 모든 콘텐츠, 디자인, 소스 코드에 대한
저작권은 Opkle에게 있습니다.
img default description