Opkle(옵클) - 창작자를 위한 앱과 시스템
옵클(Opkle)은 창작자를 위한 다양한 앱과 시스템을 제공하는 개발사입니다. 전자책 에디터 앱 'Opkle editor'를 출시했고, 관련 전자책 클래스를 제공하고 있습니다.
EDITOR
CLASS
BLOG
LOGIN
표현한다는 것의
무한한 가능성,
새로운 형태로
담아내다.
새로운 형태의 콘텐츠
Opkle은 코드 없이 웹을 마음껏 만들고, 누구나 자기 화면을 그릴 수 있도록 에디터를 만들고, 그 결과물을 어디서나 즐길 수 있게 돕는 팀입니다.
텍스트와 화면과의 조화를 통해, 웹을 짓는다는 것이 그저 단순한 코딩이 아닌, 상상을 펼치고 감각을 깨우는 과정이 될 수 있도록 좋은 도구를 만들어 냅니다.
옵클 에디터 개발기: Valkey 큐 서버의 기본 구조
dev
7
옵클 에디터 개발기: Worker loop와 AI 결과 검증
dev
8
옵클 에디터 개발기: 미리보기가 그대로 코드로
dev
9
옵클 에디터 개발기: 그래픽 에디터도 결국 직접 만들기로 했다
dev
10
옵클 에디터 개발기: Rust 공장을 그래픽 엔진으로 다듬다
dev
11
7
8
9
10
11
...
8
옵클 에디터 개발기: Worker loop와 AI 결과 검증
큐 서버의 기본 구조를 만든 뒤에는, 실제 worker가 어떤 방식으로 결과를 만들고 검증할지 정리해야 했습니다. 요청을 넣고 용량을 잡는 것만으로는 부족합니다. AI 작업은 결과 형식이 흔들릴 수 있고, 외부 생성 작업은 오래 걸릴 수 있으며, 미디어 결과는 잠깐 살아 있는 외부 링크로 끝나면 안 됩니다. worker는 요청을 실행하는 곳이면서 동시에 결과를 제품이 쓸 수 있는 형태로 정리하는 마지막 방어선이어야 했습니다.
저는 worker를 단일 함수 하나로 몰아넣지 않았습니다. 작업 도메인마다 실행기가 다릅니다. 공통적으로는 도착 신호를 모으고, 경로별로 묶고, 최신 의도만 고르고, 용량과 배타성을 잡은 뒤 비동기 실행으로 넘깁니다. 하지만 그 뒤의 계산은 도메인마다 달라집니다. 어떤 도메인은 메시지 조립과 구조 검증이 중심이고, 어떤 도메인은 결과 흡수와 자산화가 중심이며, 어떤 도메인은 비동기 완료 관측이 핵심입니다.
이 구조에서 중요한 원칙은 하나였습니다. AI가 만든 결과도 에디터의 기존 document state와 동일한 검수 흐름을 통과해야 합니다. 구조 보존형 변환은 분할 단위를 깨면 안 되고, 명령형 결과는 기계가 적용 가능한 구조여야 하며, 카탈로그 기반 결과는 허용된 명령 집합 안에 있어야 합니다. 미디어 결과는 만료되는 외부 참조가 아니라, 에디터가 장기적으로 소유할 수 있는 자산이어야 합니다. queue worker는 모델 응답을 전달하는 proxy가 아니라, 에디터 계약에 맞게 결과를 정규화하는 계층입니다.
Domain executors
텍스트 계열 executor는 먼저 도착한 신호들을 모읍니다. 그다음 경로별로 묶고, 각 경로의 최신 의도만 실행 대상으로 고릅니다. 같은 경로로 들어온 여러 요청 중 최신만 살아남고, 나머지는 종료된 결과로 닫힙니다. 이 구조는 사용자의 빠른 반복 클릭이나 연속 요청을 자연스럽게 처리합니다.
대상을 잡은 뒤에는 용량을 확보합니다. 용량을 못 잡으면 최신 의도는 다시 대기열로 돌아가고, 오래된 요청만 닫습니다. 용량을 잡았더라도 경로 배타성을 못 잡으면 같은 경로가 이미 처리 중이라는 뜻입니다. 그때는 용량을 반납하고 다시 대기시킵니다. 배타성까지 잡아야 비로소 실행에 들어갑니다. 이 순서는 자원 누수와 중복 실행을 줄이기 위한 상태기계의 진입 조건입니다.
실제 메시지 조립은 도메인마다 다릅니다. 어떤 작업은 강한 보존 제약을 걸고, 어떤 작업은 구조적 경계를 유지한 채 변환하며, 어떤 작업은 원문의 문체와 톤을 유지한 채 품질을 올립니다. 어떤 작업은 기존 본문 뒤에 이어지는 생성을 요구합니다. 겉으로는 비슷한 AI 요청처럼 보여도, 허용 자유도와 검증 기준이 다릅니다.
명령 해석형 작업은 본문 생성형 작업과 입력을 다르게 둡니다. 본문을 고쳐 쓰는 문제와, 선택 문맥을 보고 편집 가능한 명령을 만드는 문제를 같은 메시지로 섞으면 모델이 두 목표를 혼동합니다. 시각 추출형 작업은 텍스트 executor 경로에 붙어 있어도 실제로는 다른 검증 공간을 가집니다. executor는 mode를 보고 메시지뿐 아니라 validation 전략까지 바꿉니다.
Deterministic gate
모든 요청을 LLM으로 보내는 것은 좋은 설계가 아니었습니다. 어떤 요청은 이미 규칙으로 확정할 수 있습니다. 사용자가 입력한 문장이 허용된 명령 집합의 원소와 정확히 일치하면, 생성 모델이 필요하지 않습니다. 그 자리에서는 결정적 사상만으로 충분합니다.
그래서 generative path 앞에 deterministic gate를 두었습니다. 입력이 확정 명령으로 해석되면 모델을 호출하지 않고 즉시 결과를 만듭니다. 같은 명령이라도 출력 계약이 다르면 gate 결과의 형태도 달라집니다. 중요한 것은 “무엇을 했는지”가 아니라, 프론트가 기대하는 출력 공간을 지키는 것입니다.
이 gate는 단순한 최적화가 아닙니다. 비용과 지연을 줄이는 효과도 있지만, 더 중요한 것은 결정적 동작을 결정적으로 처리한다는 점입니다. 확정 가능한 편집 명령에 생성 모델의 자유도를 주면, 오히려 불확실성이 생깁니다. AI 기능 안에서도 deterministic path와 generative path를 분리해야 제품이 안정적입니다.
프론트에도 같은 판정을 둘 수 있지만, 서버에도 같은 방어선을 두었습니다. 클라이언트 우회나 버전 차이가 있어도, 확정 명령은 서버에서 안정적으로 처리되어야 합니다. gate로 끝나든 모델로 가든 cleanup 규칙은 같습니다. 프론트는 결과를 받는 방식만 보면 되고, 내부에서 어느 경로로 끝났는지 알 필요가 없습니다.
Structure-preserving validation
구조 보존형 변환에서 가장 중요한 것은 분할 단위의 대응이었습니다. 에디터는 문서를 일정한 단위로 관리합니다. 여러 단위를 한 번에 보내더라도, 결과는 다시 원래 개수와 순서에 맞아야 합니다. 모델이 두 단위를 합치거나, 하나를 둘로 나누거나, 중간을 빼먹으면 에디터는 결과를 안전하게 적용할 수 없습니다.
그래서 입력 배열을 하나의 문자열로 합칠 때 명시적 boundary marker를 넣었습니다. 각 단위 사이에 경계를 두고, 지시문에서도 그 경계를 보존하라고 반복합니다. 결과가 돌아오면 marker로 다시 나누고, 조각의 개수가 기댓값과 같은지 확인합니다. 맞으면 채택하고, 맞지 않으면 재시도합니다. 이는 단순 문자열 비교가 아니라, 입력과 출력 사이의 bijection을 검사하는 과정입니다.
재시도에서는 자유도를 줄입니다. 이전 출력이 경계를 깨뜨렸다는 사실과, 기대한 단위 수가 몇인지를 더 강하게 명시합니다. 자연스러운 문장보다 구조 대응이 더 중요할 때가 있습니다. 에디터에 적용되는 AI 결과에서는 특히 그렇습니다.
모든 작업에 같은 검증을 걸지는 않습니다. 분할 대응이 필수인 작업과, 단일 산문 출력으로 충분한 작업과, 구조화 객체가 필요한 작업은 성공의 정의가 다릅니다. 검증 기준을 분리해야 불필요한 재시도와 형식 오류를 줄일 수 있습니다. AI가 문장을 아무리 잘 고쳐도 단위 대응이 깨지면, 에디터 기능으로는 실패입니다.
Typed parse and catalog adherence
명령형·추출형 작업은 자연어 답변을 받으면 안 됩니다. 에디터가 바로 해석할 수 있는 구조를 반환해야 합니다. 하지만 생성 모델은 종종 장식을 붙이거나, 앞뒤에 설명을 넣거나, 비슷하지만 미묘하게 다른 필드를 만들 수 있습니다. 그래서 worker에는 sanitize와 parse 계층을 두었습니다.
parse의 첫 단계는 잡음을 제거해 후보 객체를 찾는 일입니다. 그다음 문법적으로 유효한지 확인하고, 기대한 shape를 만족하는지 봅니다. 실패하면 채택하지 않고 재시도합니다. 재시도 지시문은 “설명 없이, 약속된 구조만”을 더 강하게 요구합니다. 문장력이 아니라 형식 준수가 품질 기준이 되는 구간입니다.
카탈로그 기반 작업에서는 JSON이 유효한지뿐 아니라, 반환된 명령이 허용 집합의 원소인지도 확인합니다. 존재하지 않는 명령을 만들면 에디터가 실행할 수 없습니다. 자연어를 명령으로 바꾸는 문제에서 품질은 catalog adherence로 측정됩니다.
어떤 도메인은 hybrid 출력을 가집니다. 글쓰기 요청이면 텍스트 쪽이 채워질 수 있고, 편집 요청이면 명령 쪽이 채워질 수 있습니다. 둘 다 비어 있어도 유효한 무대응으로 볼 수 있습니다. 한 assistant가 writing request와 editing request를 함께 받을 수 있는 작업공간에서는, 출력 공간이 한쪽으로만 고정되면 오히려 표현력이 부족해집니다.
추출형 작업은 형식 검증에 더해 schema별 정규화가 필요합니다. parse가 실패하면 재시도하고, 어떤 schema에서는 형식은 맞아도 내용이 빈약한지까지 봅니다. 구조가 맞다는 것과 정보가 충분하다는 것은 다른 명제입니다.
Candidate selection
AI extraction에서 항상 마지막 시도가 가장 좋지는 않습니다. 특히 시각 정보 추출은 첫 결과가 꽤 완전하고, 다음 결과가 형식은 더 깔끔하지만 일부를 놓칠 수 있습니다. 그래서 후보 집합을 두고, 각 시도를 completeness score로 평가한 뒤 더 완전한 결과를 보존합니다.
과소추출이 의심되면 재시도 지시도 달라집니다. 단순히 “유효한 구조를 반환하라”가 아니라, 누락된 영역을 보완하라는 completeness hint를 줍니다. 형식 오류로 인한 재시도와 내용 부족으로 인한 재시도는 다른 문제입니다. 같은 재시도라도 이유가 다르면 지시가 달라져야 합니다.
마지막 시도가 실패했거나 더 나빠도, 이전에 더 완전한 후보가 있으면 그것을 살립니다. 완전한 실패보다 부분적으로라도 쓸 수 있는 결과가 낫고, 이전 시도가 더 많은 정보를 담고 있을 수 있습니다. 이 판단은 모델이 아니라 worker가 합니다. worker는 응답을 받아 적는 곳이 아니라, 후보를 비교하고 채택하는 실행 계층입니다.
사용자는 한 번의 요청만 보지만, 서버 내부에서는 여러 번의 시도와 검증, 후보 보존이 일어납니다. 깨진 응답은 버리고, 과소추출은 재시도하고, 가장 완전한 결과를 남깁니다. 누락이 사용자에게 바로 보이는 작업일수록 이 차이가 큽니다. 결국 추출 worker는 모델 호출부가 아니라, 관측·검증·선택으로 이어지는 extraction pipeline이 되었습니다.
Media materialization
이미지와 영상 worker는 텍스트 worker보다 I/O 성격이 강합니다. 생성 요청을 검증하고, 용량과 배타성을 잡은 뒤 외부 생성기를 호출합니다. 응답에서 결과 참조를 받으면 그것을 그대로 쓰지 않습니다. 외부 provider의 참조는 만료될 수 있고, 출판 에디터의 문서 자산으로 쓰기에는 불안정합니다. 그래서 즉시 흡수해, 제품이 소유하는 정적 자산으로 바꿉니다.
옵션도 서버에서 정규화합니다. 허용 범위를 넘는 값은 자르고, 명시되지 않은 값은 기본 정책을 따르며, 사용자가 의도적으로 비운 값과 아예 보내지 않은 값을 구분합니다. 이런 작은 차이가 UI 옵션과 실제 생성 결과 사이의 신뢰를 만듭니다.
편집형 미디어 작업은 입력 자산이 필수입니다. 필수 입력이 없으면 빈 결과로 종료합니다. 필요하면 입력의 구조를 압축해 생성 문맥에 붙입니다. 색, 도형, 텍스트 영역, 좌표 같은 정보는 편집 요청의 정확도를 올리는 신호가 됩니다. 이때 크기 제한과 임시 자원 정리도 함께 처리합니다.
영상 생성과 영상 편집은 더 긴 흐름을 가집니다. 필수 media가 없으면 시작하지 않습니다. 잘못된 입력이 외부 호출까지 들어가면 비용과 시간이 낭비되기 때문에, worker 입구에서 끊는 것이 맞습니다.
media worker에서 중요한 것은 결과 참조의 소유권입니다. 외부의 임시 링크를 그대로 문서에 넣으면, 나중에 책을 다시 열거나보낼 때 자산이 사라질 수 있습니다. 생성 결과는 모두 제품 쪽 저장소로 흡수합니다. 에디터는 provider URL이 아니라, 옵클이 관리하는 정적 참조를 받습니다.
Asynchronous observation
영상 작업은 즉시 결과가 나오지 않습니다. 먼저 작업을 생성하고, 관측 가능한 작업 식별자를 받아야 합니다. 식별자가 없거나 생성 자체가 실패하면 그 자리에서 종료합니다. 작업이 만들어지면 worker는 일정 간격으로 상태를 관측합니다.
상태는 크게 대기, 진행, 성공, 실패, 취소, 불명으로 나뉩니다. 대기와 진행은 계속 기다리고, 성공이면 결과 참조를 얻어 루프를 끝냅니다. 실패·취소·불명은 빈 결과로 닫습니다. 관측 중 일시적인 네트워크 오류가 나면 바로 전체 실패로 보지 않습니다. 통신 오류와 작업 실패는 다른 사건입니다.
관측 주기와 최대 대기 시간도 명확히 잡았습니다. 긴 작업이 점유하는 용량은 짧게 끝나는 작업의 용량과 분리해야 합니다. 영상이 오래 걸린다고 해서 문장 변환이나 짧은 명령 해석까지 같이 막히면 안 됩니다. 다계급 큐에서 긴 서비스 시간 클래스가 짧은 클래스를 잠식하지 않게 하는 원칙과 같습니다.
결과 참조를 받은 뒤에도 작업은 끝나지 않습니다. worker는 그 내용을 읽어 제품 자산으로 흡수합니다. 영상은 크기가 클 수 있으므로 전송 한도와 시간 한도도 여유 있게 둡니다. 최종 결과는 외부 링크가 아니라, 옵클 에디터가 관리하는 자산 참조입니다.
이 구조가 잡히면 영상 작업도 에디터의 일반 자산 처리 흐름 안으로 들어옵니다. 사용자는 긴 작업을 기다리지만, 결과가 도착하면 그것은 단순 외부 링크가 아니라 제품이 소유한 자산입니다. 이후 미리보기에서 재생하든, 책 안으로 묶든 안정적으로 다룰 수 있습니다.
Resource release invariants
큐 서버에서 가장 중요하게 본 것 중 하나는 cleanup path였습니다. AI 작업은 실패할 수 있습니다. 외부 호출이 실패할 수도 있고, 구조 검증이 실패할 수도 있고, 자산 흡수가 실패할 수도 있습니다. 하지만 실패했다고 해서 용량이나 경로 배타성이 남으면 다음 요청이 막힙니다. 그래서 실행부는 항상 성공·실패와 무관한 해제 구간을 갖도록 감쌌습니다.
작업이 끝나면 성공 여부와 관계없이 in-flight 등록을 지우고, 경로 배타성을 풀고, 용량을 반납하고, 경로 lock도 해제합니다. 이 순서는 모든 executor에서 반복됩니다. 하는 일이 달라도 종료 불변식은 같아야 합니다. 그래야 장애 상황에서 어디를 봐야 하는지 명확합니다.
빈 결과도 결과입니다. 입력이 비었거나 필수 자산이 없거나, 생성이 없거나, 흡수가 실패하면 빈 종료 상태를 보냅니다. 중요한 것은 클라이언트가 계속 기다리지 않게 하는 것입니다. 사용자는 실패했더라도 완료 상태를 받아야 합니다.
실시간 결과 저장소에는 수명을 둡니다. UI가 가져갈 시간은 보장하되, 런타임 상태에 결과가 끝없이 쌓이지 않게 합니다. 장기 기록은 별도 로그 계층이 맡습니다. 빠른 실행 상태와 느린 이력을 나누어야, 각각이 자기 역할에 집중할 수 있습니다.
신호만 남고 실제 최신 의도가 없는 경우도 cleanup의 대상입니다. 이미 용량을 잡고 진행 중인 식별자는 건드리지 않고, 더 이상 유효하지 않은 오래된 요청만 닫습니다. 어떤 경우에도 pending 상태를 방치하지 않는 것이 원칙이었습니다.
Control loop
최종 worker loop는 계속 도는 단순한 구조입니다. 한 tick에서 각 작업 도메인의 executor를 순서대로 호출합니다. 각 executor는 자기 대기열을 비우고, 경로별 grouping을 하고, 최신 의도를 고르고, 용량과 lock을 잡은 뒤 실제 작업을 비동기로 실행합니다. loop 자체는 너무 많은 일을 하지 않고, 각 executor의 정책만 실행합니다.
loop의 휴지 간격은 짧게 둡니다. 너무 길면 사용자가 AI를 눌렀을 때 시작 지연이 커집니다. 대신 stale lease를 회수하는 sweep은 매 tick마다 하지 않고 일정 주기로 수행합니다. 평소 loop는 가볍게 유지하고, 오래된 등록과 lock은 주기적으로 정리합니다.
worker 시작 시에도 sweep을 한 번 수행합니다. 이전 프로세스가 비정상 종료되면서 남긴 in-flight 흔적이 있을 수 있기 때문입니다. 대기열에 아직 들어 있는 작업은 다시 처리하면 되지만, stale registry가 남아 있으면 경로가 막힐 수 있습니다. 시작 시 sweep은 깨끗한 상태로 들어가기 위한 초기 정리입니다.
종료 신호도 처리합니다. 신호를 받으면 새 loop를 시작하지 않게 하고, 이미 실행 중인 비동기 작업은 자기 해제 경로를 통해 정리됩니다. 재시작 안전성은 in-flight 집합과 연결됩니다. 진행 중인 작업이 없으면 안전하게 다시 시작할 수 있고, 있으면 기다릴 수 있습니다. 아직 집어 가지 않은 작업은 대기열에 남아 있으므로 손실 대상이 아닙니다.
이 단계까지 오면서 큐 서버는 단순 background process가 아니라 AI runtime infrastructure가 되었습니다. 경로별 최신 의도, 용량과 lock, in-flight 등록, 재시도 검증, 자산 흡수, 비동기 관측, 결과 수명, 이력 기록, stale sweep, graceful shutdown이 하나로 연결되었습니다. 반복 테스트에서도 구조 보존, 형식 검증과 재시도, 결정적 gate, 최신 의도 우선, 오래된 요청 종료, 긴 작업의 관측, 자산화, 재시작 안전성, cleanup path가 안정적으로 통과했습니다. 옵클 에디터의 AI 기능은 이제 모델 호출이 아니라, 제품 환경에서 지속적으로 버틸 수 있는 worker system 위에서 작동하게 되었습니다.
이전글
목록으로
다음글
저작권 고시
Copyright Notice
본 웹사이트의 모든 디자인 결과물 및 영상에 대한 저작권은 Abstract Cloud에 있으며, 저작권법 및 관련 법령에 의해 보호받습니다. 웹, 영상, 본문, 표지, 내지 디자인을 포함한 모든 콘텐츠는 저작권자의 자산으로, 사전 동의 없이 무단 복제, 배포, 2차 저작물 제작, 온라인 공유 등을 금지합니다. 이를 위반할 시, 저작권법에 따라 민형사상 책임을 질 수 있습니다. 정당한 구매와 저작권 보호는 창작자의 권리를 지키며, 더 나은 작품으로 보답할 힘이 됩니다.
저작권자: Abstract Cloud | 대표자: 배창규(uragen)
© Abstract Cloud. All Rights Reserved.
HOME
FAQ
이용 약관
개인정보 이용방침
help@opkle.app
010-2747-3403
상호 :
추상적 형상 디자인(Abstract cloud) |
대표자 :
배창규
사업자등록번호 :
249-74-00533
통신판매업 신고번호 :
2025-의정부송산-0634
주소 :
경기도 의정부시 부용로 49, 108동 402호
웹의 모든 콘텐츠, 디자인, 소스 코드에 대한
저작권은 Opkle에게 있습니다.