[졸프] Spring Boot로 GPT API 질문 생성 재시도 로직 구현기 (쿨다운·최대횟수·예외처리 포함)

ssun·2025년 8월 16일

졸업프로젝트

목록 보기
16/21

1. 배경

< 현재 question 테이블 스키마>

retry_count ,last_retry_at ,last_error 은 질문 생성 재시도 로직에서만 사용

  • retry_count → 지금까지 재시도된 횟수 카운트
    • 재시도 횟수는 3회로 설정
  • last_retry_at → 마지막으로 재시도 버튼을 누른 시각
    • 사용자가 버튼을 연타할 때 last_retry_at 기반으로 "1분 내 재시도 금지" 제약 구현 (쿨다운을 위해서)
  • last_error → 직전 실패 사유 (ML 응답 오류, 타임아웃, 예외 메시지 등)

→ CRUD 같은 일반 질문 등록·조회·좋아요·스크랩 같은 기능에는 전혀 관여하지 않음

해당 칼럼들이 재시도 API 흐름에서 언제/어디서 쓰이나?

1) 재시도 진입(서비스) – 읽고(판정), 쓰는(전환) 포인트

(A) 진입 검증 단계 – “재시도 가능한지 판단”

  • 상태 확인: status == FAILED 인지 체크 (아니면 409)
  • 비재시도 오류 차단: last_error 접두로 판정
    • ML_400, ML_401, ML_403, ML_404, VALIDATION 등 → 즉시 409 (“데이터/권한 문제로 재시도 불가”)
  • 최대 재시도 횟수 제한: retry_count >= retryMaxCount(기본 3)409
  • 쿨다운(레이트 리밋): now() < last_retry_at + cooldown(기본 60초)409(남은 초 안내)

이 단계에서 읽는 컬럼: status, retry_count, last_retry_at, last_error

(B) 재시도 시작 단계 – “실행 전 상태 전환”

  • FAILED → GENERATING 로 전환하면서 아래 3가지 ‘갱신’:
    • retry_count = retry_count + 1
    • last_retry_at = now()
    • last_error = null (새 시도이므로 직전 실패 사유 초기화)
  • 커밋 후 이벤트 발행 → 비동기 생성 실행기 시작

이 단계에서 쓰는 컬럼: retry_count, last_retry_at, last_error(null로 초기화), status


2) 백그라운드 생성 실행기 – 실패 사유 기록

  • ML 호출 성공 → status = READY (세 메타는 그대로 둠)
  • ML 호출 실패:
    • HTTP 4xx (요청/권한/데이터 문제): last_error = "ML_4xx: <요약메시지>" → 이후 재시도 진입 시 즉시 차단 근거
    • TIMEOUT / 5xx / 네트워크: last_error = "TIMEOUT: 3s exceeded" 또는 "ML_5xx: ..."재시도 허용(쿨다운·상한에만 걸림)
    • status = FAILED 로 반영

이 단계에서 쓰는 컬럼: last_error, status


2. 핵심 정책 정리

  • 동일 도서에 GPT 질문이 이미 READY새로 만들지 않음409 Conflict
  • 동일 도서에 GENERATING 진행 중기존 진행 건 공유200 OK + questionId
  • 동일 도서에 FAILED 존재자동 재시도 안 함200 OK + questionId (FAILED 상태 → 재시도 버튼 안내)
  • 둘 다 없으면드래프트 행 생성(GENERATING, userNickname='GPT') → 201 Created
    • 동기 2~3초 내 생성 완료 시 READY 업데이트 후 201
    • 타임아웃 시 GENERATING 상태로 questionId 반환(201), 이후 백그라운드에서 계속 진행
  • 예외
    • 도서 없음 → 404
    • GPT 시스템 계정 미초기화 → 500
    • 중복 INSERT 레이스 → 유니크 충돌 시 재조회로 회복

3. 질문 생성 + 재시도 로직

3-1. 요청 수신

  • POST /api/books/{bookId}/generate-question
  • bookId로 도서를 조회 → 없으면 404

3-2. 기존 질문 확인 (비관적 락)

  • 동일 bookId + GPT 사용자 행 조회
  • READY → 이미 존재 → 409 Conflict
  • GENERATING → 진행 중 → 200 OK (questionId 반환)
  • FAILED → 자동 재시도 안 함 → 200 OK (FAILED 상태 반환)

3-3. 신규 드래프트 생성

  • 위 3가지 상태 모두 없으면 → status=GENERATING으로 row 생성
  • 극단적 레이스 시도(동시 INSERT) 발생 → 유니크 충돌 시 재조회 후 동일 정책 적용

3-4. 동기 타임박스 생성

  • ML 서버 호출 (2~3초 제한)
  • 성공 시 → 질문 내용 채워서 READY 저장, 201 Created
  • 실패/타임아웃 시 → 그대로 GENERATING 반환, 201 Created

3-5. 백그라운드 Worker

  • 비동기(@Async)로 ML 호출 이어서 진행
  • 성공 시 READY 업데이트
  • 실패 시 FAILED 업데이트

3-6. 조회/재시도 정책

  • 여러 사용자가 동시에 요청해도 최초 요청만 GPT 호출
  • 나머지는 동일 questionId와 상태를 공유
  • FAILED인 경우 → UI에서 “재시도” 버튼 노출
    • 재시도 시 동일 API 호출 → 기존 row 상태 확인 후 다시 진행

이렇게 로직을 작성하였다. 하지만 여기서 의문점이 들었다. 생성과 생성 재시도는 어떤 차이가 있을까?

4. 질문 생성과 재시도의 차이

우선 생성과 재시도 모두 authenticated() 설정을 통해서 보호 경로로 설정하였다.
하지만 생성은 GPT 시스템 사용자가 자동으로 실행하는 것이고, 재시도는 사용자가 직접 요청을 하는 방식이다.

하지만 나는 GPT 시스템 사용자는 로그인이 불가(토큰 발급 X)하도록 설정을 해놓았다. 그렇다면 둘다 보호경로이지만 Bearer 토큰은 필요없는걸까? 결론부터 말하면 재시도만 토큰이 필요하다. 정리하자면 다음과 같다.

1. 질문 생성 (GPT 자동 생성)

  • 주체: GPT 시스템 사용자
  • 특징:
    • 로그인 불가, 토큰 발급 X (사람이 직접 로그인하지 않는 계정)
    • 서버 내부에서만 사용됨 (예: QuestionGenerationService에서 gptUserId를 강제로 세팅해서 INSERT)
  • 따라서 Bearer 토큰 필요 없음. 이건 “사용자가 API를 치는 행위”가 아니라, 백엔드 내부에서 자동 실행되는 로직이라서 인증 경로를 탈 필요 X

2. 질문 재시도

  • 주체: 실제 로그인한 사용자(일반 유저)
  • 특징:
    • FAILED 상태의 질문을 보고, “다시 시도해봐야겠다” 하고 프론트에서 API 호출
    • 이건 “누가 버튼 눌렀는지”를 알아야 하고, 일반 사용자의 액션으로 트리거되는 API이지만 서비스 기획상 흥미를 일으키기 위해서 permitAll로 설정

결론

  • 생성 API (서버 내부에서 GPT 자동 실행): Bearer 토큰 필요 없음
  • 재시도 API (사용자 액션으로 요청): Bearer 토큰 필요없음

이렇게 둘다 permitAll로 설정했다.

그리고 ML과 프엔 담당자에게 아래와 같이 전달하였다.

"저는 우선 질문 생성, 질문 생성 재시도 코드는 구현 완료했습니다! 
아직 ML 서버는 키고 테스트 해보지 않아서 다음주에 테스트 예정입니다
질문 생성 재시도 로직은 아래 노션 링크 걸어두겠습니다. 

그리고 다른 API와는 다르게 질문 생성/재시도 API는 GPT API를 사용하는 만큼 트래픽가 생길 경우를 
대비해서 question 테이블 내에 retry_count ,last_retry_at ,last_error 칼럼들을 추가했습니다. 
이 칼럼들은 재시도 기능의 안정성을 위해서 추가해놓은 겁니다. 

해당 칼럼들을 이용해서 질문 생성이 실패했을 경우에 재시도 횟수(retry_count 이용)는 3번,
한번 재시도 했을 경우에는 쿨다운을 위해서 “1분 내 재시도 금지” 제도(last_retry_at 이용)를 
걸어놓았습니다. 

그리고 재시도 했을 경우에 4XX 에러가 나는 경우에는 “요청을 보낸 쪽(우리 서버 or 입력 데이터)에 
문제가 있다는 뜻.”이기에 재시도 해도 같은 에러가 발생됩니다. 
그래서 이 경우에는  바로 재시도 차단을 해놓았습니다. 
5xx 케이스와 타임아웃 에러인 경우에는 일시적인 서버 장애 또는 외부 서비스의 지연이 원인이라 
쿨다운 후 다시 시도하면 성공할 가능성이 있기 때문에 재시도 가능하게 구현해놓았습니다. "

그리고 현재 비관적 락을 사용하였지만, 추가적으로 필요할 경우에는 낙관적 락까지 구현해볼 예정이다. 아직 ML과의 연동을 본격적으로 테스트 해보지는 않았기에 걱정이 되지만 코드 초안은 모두 구현하였다. 그래도 졸업프로젝트를 하면서 정말 많은 걸 배워가서 뿌듯하다. 지난 학기에 수강한 '컴퓨터 아키텍처'라는 수업에서 배웠던 트래픽 관련 문제를 조금씩 다루게 되어서 신기하기도 하다.

다음주 연동테스트 홧팅...🫠

profile
안녕하세요! 백다현입니다

0개의 댓글