30초로는 부족했다
- 기능: 문서 OCR 파싱
- 기존 구조: 내부 OCR 서비스에 HTTP 요청 → 결과 수신 → 응답 (동기)
30초 타임아웃에 계속 걸린 원인 두 가지:
- PaddleOCR 콜드스타트 — 모델 로딩 지연. 유휴 후 첫 요청이 특히 느림
- 페이지당 처리 지연 — 문서 길이에 선형 비례
타임아웃 연장을 기각한 이유:
- 페이지 수에 비례하는 작업이라 상한 설정 불가
-
요청 스레드와 커넥션을 그동안 점유
- 조치: webhook 콜백 구조로 전환
바뀐 흐름
[이전] 클라이언트 → 서버 → OCR 서비스 (동기 대기 30초+) → 응답
[이후] 클라이언트 → 서버 → OCR 서비스 async 제출 → jobId 즉시 반환
OCR 서비스 완료 → 서버 /callback → 결과 저장
클라이언트 → GET /requests/{id} 로 조회
-
jobId매핑 엔티티 신설 - 저장 항목: 상태, 결과 JSON(
MEDIUMTEXT) - 클라이언트는 즉시
jobId수신 후 조회 엔드포인트로 진행 상태 확인
이 구조를 만들면서 다룬 논점 세 가지:
(1) 콜백 멱등성
콜백은 반드시 중복해서 온다고 가정해야 한다.
- OCR 서비스의 재시도 가능성
- 네트워크 문제로 응답 유실 → 상대가 실패로 판단 후 재전송
조치:
-
jobId기준으로 기 처리된 요청이면 무시 -
jobId는 OCR 서비스가 발급하는 자연스러운 멱등 키 — 별도 설계 불필요
후속 커밋에서 추가한 것:
-
jobIdnull 가드 - 이유:
findByJobId(null)호출 시 의도치 않은 행에 매칭될 수 있음 - 전제: 콜백 페이로드는 외부 입력이므로 필드 누락 가능성을 상정
보안 처리:
- 콜백 경로는 세션 인증 대상 아님 →
SecurityConfig에서 인증 예외 -
대신 내부 전용 API 키 헤더 검증 추가
- 원칙: 인증 예외로 열어둔 엔드포인트에는 반드시 다른 형태의 검증 동반
(2) 트랜잭션 경계
- 경위: 처음에 잘못 잡았다가 리뷰에서 수정
기존 문제:
- 서비스 클래스에
@Transactional부착 - 외부 OCR 서비스 호출이 트랜잭션 안에서 실행
- 외부 HTTP 호출은 응답 시간 예측 불가
- 그동안 DB 커넥션 점유 → 커넥션 풀 고갈의 전형적 경로
그 후속 커밋의 정리 내용:
| 대상 | 변경 |
|---|---|
클래스 @Transactional
|
제거 |
| 제출(submit) | OCR 호출 먼저 → jobId 받아 1회 INSERT |
| 콜백(handleCallback) |
@Transactional 명시 |
핵심은 제출 경로의 순서 변경:
- 1단계: 외부 호출로
jobId수신 — 트랜잭션 밖 - 2단계: 그 결과를 가지고 트랜잭션 안에서 1회 INSERT
콜백 쪽은 반대로 @Transactional 명시:
- 조회한 엔티티의 상태·결과를 더티 체킹으로 반영
- 트랜잭션 부재 시 영속성 컨텍스트가 요청 범위를 벗어나 변경 미저장 가능
“외부 호출은 트랜잭션 밖, 저장은 트랜잭션 안” 이 원칙이 결국 두 메서드에 서로 다른 처방으로 나타난 셈이다. 클래스 레벨 @Transactional은 이 구분을 지워버린다.
(3) 점진적 마이그레이션
기존 동기 엔드포인트를 그대로 남겼다.
- 신규 추가: 기존 경로에
/async를 붙인 비동기 엔드포인트 2개 -
함께 배치: 조회 · 콜백 엔드포인트
- 이유: 프런트엔드 준비 전 서버가 응답 형태를 바꾸면 그 시점부터 화면 파손
비동기 전환 = 클라이언트 입장에서 응답 계약이 완전히 달라지는 변경:
- 결과를 받던 자리에
jobId가 반환됨 - 폴링 로직 신규 필요
병존의 이점:
- 서버 배포와 클라이언트 전환 분리 가능
- 짧은 문서는 동기, 긴 문서만 비동기로 선택 가능
- 동기 경로 제거 시점은 클라이언트 전환 완료 후 결정
남는 교훈
동기를 비동기로 바꾸는 작업의 실제 난이도는 “비동기로 만드는 것”에 있지 않다. 즉시 jobId를 반환하는 건 쉽다. 어려운 건 그 뒤에 생기는 새로운 경계들이다.
- 응답이 두 번(제출 응답 + 콜백)으로 분리 → 그사이 상태의 저장소 필요
- 콜백은 네트워크 너머에서 도착 → 중복 · 유실 · 인증 전부 처리 필요
- 외부 호출과 DB 트랜잭션이 혼재 → 커넥션 장시간 점유
- 클라이언트 계약 변경 → 일괄 전환 불가
이번 작업에서 실제로 리뷰에 걸린 것도 비동기 전환 자체가 아니라 트랜잭션 경계와 null 가드였다. 구조를 바꾸는 커밋보다 그 구조가 만드는 경계를 다듬는 후속 커밋이 더 촘촘했다.