Post

[SAN] 비동기 작업 흐름 전체 정리

CARD_ANALYSIS, SCRAP_REFINE, TIL_GENERATION, TIL_GITHUB_COMMIT, RECALL_QUIZ_GENERATION, GITHUB_STAR_RECOMMENDATION 6개 JobType의 등록부터 처리까지 전체 흐름을 정리합니다.

[SAN] 비동기 작업 흐름 전체 정리

현재 구현된 JobType 6종의 전체 흐름을 정리합니다. 공통 인프라를 먼저 설명하고, 각 잡 타입의 등록과 처리 흐름을 순서대로 기술합니다.


공통 인프라

async_jobs 테이블

컬럼설명
job_idUUID (PK)
job_type6종 JobType 중 하나
target_id작업 대상 엔티티 ID (job_type에 따라 의미가 다름, FK 없음)
statusPENDINGPROCESSINGCOMPLETED / FAILED
error_message실패 시 cause chain 기록

중복 방지 — 유니크 부분 인덱스

앱 기동 시 ApplicationRunner가 아래 인덱스를 생성합니다. 동일 (target_id, job_type) 조합의 활성 잡이 두 개 이상 등록되는 것을 DB 레벨에서 막습니다. 충돌 시 DataIntegrityViolationException이 발생하고, AsyncJobManagerDUPLICATE_RESOURCE 예외로 변환합니다.

1
2
3
CREATE UNIQUE INDEX IF NOT EXISTS uk_async_jobs_active_target_job_type
    ON async_jobs (target_id, job_type)
    WHERE status IN ('PENDING', 'PROCESSING')

상태 전이 공통 패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[등록]
  → AsyncJobManager.enqueue()           -- REQUIRES_NEW (기본)
    또는 enqueueInCurrentTransaction()  -- MANDATORY (TilService만 사용)
  → async_jobs INSERT (PENDING) + 이벤트 발행
  ← 202 반환 (jobId, targetId 포함)

[비동기 처리]
  @TransactionalEventListener(AFTER_COMMIT) + @Async(전용 Executor)
  → PROCESSING 전환
  → 실제 작업 수행
  → COMPLETED 또는 FAILED 전환 (실패 시 cause chain 기록)

[클라이언트 폴링]
  GET /async-jobs/{jobId}
  ← { jobId, jobType, status, errorMessage }

배치 복구 (2시간마다)

서비스역할
GhostJobRecoveryServicePENDING 30분·PROCESSING 1시간 초과 잡 → FAILED 후 재등록
FailedJobRecoveryServiceFAILED 잡 최대 3회까지 재등록
OrphanScrapRecoveryService지식카드 없는 스크랩 → CARD_ANALYSIS 재등록
OrphanScrapRefineRecoveryService정제되지 않은 스크랩 → SCRAP_REFINE 재등록

1. CARD_ANALYSIS — 지식카드 AI 분석

스크랩 저장 시 자동으로 등록됩니다. target_idscrap_id입니다.

등록

POST /scraps 요청이 들어오면 원본 내용을 정규화(CRLF→LF, trim)하고 SHA-256 해시로 중복을 판별합니다.

  • 기존 스크랩: 지식카드 유무를 확인해 없으면 활성 잡이 있는지 조회하고, 없을 때만 새 잡을 등록합니다.
  • 신규 스크랩: scraps INSERT 후 CARD_ANALYSIS와 SCRAP_REFINE을 동시에 등록합니다.

두 경우 모두 202와 함께 scrapId, analysisJobId, refineJobId를 반환합니다.

처리 (aiJobExecutor)

스크랩 원문을 AI 서버에 전달해 title, summary, tags, category, embedding(1536차원)을 받습니다. 카테고리와 태그를 조회 또는 생성한 뒤 knowledge_cards에 저장합니다.

완료 후 조회

  • GET /cards/{scrapId} — 생성된 카드 ID 조회
  • GET /cards/{cardId}/similar-cards — 코사인 거리 기반 유사 카드 조회 (threshold 0.5, 결과 없으면 fallback으로 가장 가까운 K개 반환)

2. SCRAP_REFINE — 스크랩 원본 정제

CARD_ANALYSIS와 함께 스크랩 저장 시 자동으로 등록됩니다. target_idscrap_id입니다.

처리 (aiJobExecutor)

스크랩 원본을 AI 서버에 전달해 정제된 내용(refinedContent)을 생성하고 scraps를 업데이트합니다. 원본 저장 성공 여부가 AI 정제 성공에 의존하지 않도록 CARD_ANALYSIS와 별개의 잡으로 분리되어 있습니다.


3. TIL_GENERATION — TIL 문서 생성

사용자가 특정 날짜의 TIL 생성을 요청할 때 등록됩니다. target_idsummary_id입니다.

등록

POST /til { targetDate } 요청 시 두 가지 중복 방지를 거칩니다.

  1. daily_summaries 행에 SELECT FOR UPDATE를 걸어 동시 생성 요청을 직렬화합니다.
  2. 동일 사용자의 활성 TIL 생성 잡이 있으면 409를 반환합니다.

통과하면 daily_summaries를 INSERT하고 같은 트랜잭션 안에서 잡을 등록합니다(enqueueInCurrentTransaction — MANDATORY). 6개 JobType 중 유일하게 부모 트랜잭션에 참여하는 방식으로, daily_summaries 생성과 잡 등록의 원자성을 보장합니다.

처리 (aiJobExecutor)

해당 날짜의 지식카드 원문을 모아 AI 서버에 전달하면 title, tilMarkdown, embedding(1536차원)을 받습니다. daily_summaries를 업데이트해 TIL을 완성합니다.

완료 후 조회

  • GET /til?date=YYYY-MM-DD — TIL 목록 조회
  • GET /til/{summaryId}/recall-cards — TIL 임베딩 기반 리콜 카드 조회 (threshold 0.5, 원본 카드 제외, fallback 포함)

4. TIL_GITHUB_COMMIT — TIL GitHub 커밋

사용자가 생성된 TIL을 GitHub에 푸시를 요청할 때 등록됩니다. target_idtil_github_commit_id입니다. async_jobs와 별개로 til_github_commits 테이블이 자체 상태(PENDINGPROCESSINGCOMPLETED/FAILED)를 관리합니다.

등록

POST /til/{summaryId}/github-commits 요청 시 아래 순서로 검증합니다.

  1. TIL 생성 완료 여부 확인 (미완료면 400)
  2. GitHub 계정 연동 여부 확인
  3. 연결된 레포지토리 정확히 1개인지 확인
  4. contentHash 기준 중복 커밋 확인 (PENDING/PROCESSING/COMPLETED이면 409)
  5. GitHub API로 파일명 충돌 회피 후 filePath 결정
  6. til_github_commits INSERT + async_jobs 등록

처리 (githubJobExecutor)

async_jobstil_github_commits 양쪽을 PROCESSING으로 전환한 뒤, 암호화된 GitHub 액세스 토큰을 복호화해 PUT /repos/{owner}/{repo}/contents/{filePath} 요청을 보냅니다. 성공하면 커밋 SHA와 URL을 기록하고 양쪽 테이블을 COMPLETED로 전환합니다.

GitHub PUT은 동일 경로 재요청 시 409 Conflict가 발생할 수 있으므로 aiJobExecutor와 별도 풀(githubJobExecutor)로 분리했습니다. 비동기 컨텍스트에서 til_github_commits 상태 업데이트는 TransactionTemplate으로 독립 트랜잭션을 사용합니다.


5. RECALL_QUIZ_GENERATION — 리콜 퀴즈 생성

사용자의 리콜 퀴즈 생성 요청 또는 TIL 자동 생성 배치에서 등록됩니다. target_idgeneration_id입니다.

처리 (aiJobExecutor)

recall_quiz_generations에서 대상 날짜와 퀴즈 타입(OX, SHORT_ANSWER)을 읽어 AI 서버에 퀴즈 생성을 요청하고 결과를 저장합니다.


6. GITHUB_STAR_RECOMMENDATION — GitHub Star 추천 분석

사용자의 GitHub Star 추천 요청 시 등록됩니다. target_iduser_id입니다.

처리 (aiJobExecutor)

사용자의 GitHub Star 목록을 분석해 학습에 유용한 레포지토리를 추천합니다. 결과는 별도 테이블에 저장됩니다.


흐름 비교

 CARD_ANALYSISSCRAP_REFINETIL_GENERATIONTIL_GITHUB_COMMITRECALL_QUIZ_GENERATIONGITHUB_STAR_RECOMMENDATION
진입점POST /scraps (자동)POST /scraps (자동)POST /tilPOST /til/{id}/github-commits별도 API별도 API
target_idscrap_idscrap_idsummary_idtil_github_commit_idgeneration_iduser_id
외부 호출AI (분석+임베딩)AI (정제)AI (TIL+임베딩)GitHub APIAI (퀴즈)AI (추천)
스레드 풀aiJobExecutoraiJobExecutoraiJobExecutorgithubJobExecutoraiJobExecutoraiJobExecutor
추가 상태 테이블til_github_commits
enqueue 방식REQUIRES_NEWREQUIRES_NEWMANDATORYREQUIRES_NEWREQUIRES_NEWREQUIRES_NEW
중복 방지content_hash + 유니크 인덱스유니크 인덱스SELECT FOR UPDATE + 활성 잡 조회content_hash 조회유니크 인덱스유니크 인덱스
This post is licensed under CC BY 4.0 by the author.