이번 졸업 프로젝트에서는 도서 기반 질문 생성 서비스를 ML 서버와 연동해서 구현하고 있다.기존 질문 생성 API 명세서는 프엔과 맞춰본 것이기 때문에, ML과도 request,response 값을 같이 맞추어야했기 때문에 해당 명세서를 작성했다.
초기 버전에서는 ML 서버 응답을 단순히 String(질문 텍스트)로만 받아 저장했는데,
실제 명세에서는blocked:true/false, reason, latencyMs 등이 포함되어 있었다.
즉, 정책 차단(금칙어/무의미 질문 등) 케이스를 반영하지 못하고 있었던 것이다.
이번 글에서는 그 부분을 어떻게 리팩토링했는지 과정을 정리한다.
기존 구현에서는:
private String trySyncGenerate(Book book) {
return mlWebClient.post()
.uri("/generate-question")
.bodyValue(req)
.retrieve()
.bodyToMono(String.class) // 질문을 String으로만 가정
.block();
}
→ 이렇게 처리하다 보니, ML 응답의 blocked/reason 필드를 전혀 반영하지 못함.
결과적으로 정책 차단된 질문도 그대로 DB에 저장하는 문제가 있었다.
명세에 맞게 요청/응답 DTO를 추가했다.
// 요청 DTO
public record MlGenerateReq(Long bookId, String title) {}
// 응답 DTO
public record MlGenerateResp(
Long bookId,
boolean blocked,
String question, // blocked=false일 때만 유효
Integer latencyMs, // optional
String reason // blocked=true일 때 optional
) {}
이제 ML 응답을 MlGenerateResp로 파싱하고, blocked 분기에 따라 상태 전이를 처리한다.
MlGenerateResp resp = trySyncGenerate(book);
if (resp.blocked()) {
// 정책 차단 → FAILED
draft.markFailed("blocked:" + resp.reason());
return Result.created201(qid, FAILED, null);
} else {
// 정상 생성 → READY
draft.setQuestionContent(resp.question());
draft.setStatus(READY);
return Result.created201(qid, READY, null);
}
에러 메시지/질문을 DB에 안전하게 저장하기 위해 유틸 메서드를 추가했다.
private String safe(String s) {
return s == null ? "" : s.replace("\"","'");
}
private String truncate(String s, int max) {
return s == null ? null : (s.length() <= max ? s : s.substring(0, max));
}
최종적으로 서비스는 다음과 같이 동작한다.
| 상황 | ML 응답 | 최종 상태 | DB 기록 |
|---|---|---|---|
| 정상 생성 | 200 + blocked=false | READY | questionContent 저장 |
| 정책 차단 | 200 + blocked=true | FAILED | lastError = reason |
| ML 오류 | 4xx/5xx | FAILED | lastError = 예외 메시지 |
| 타임아웃 | 응답 지연 (3s 초과) | FAILED* | lastError = timeout |
* 타임아웃의 경우: 우선 GENERATING 상태로 두고 백그라운드 재시도 후 READY/FAILED로 최종 전이 가능
리팩토링 이후에는:
이번 리팩토링의 핵심은 “ML 응답을 단순 String → DTO 기반으로 바꾸고, blocked 분기를 반영”이었다.
덕분에 정책 차단 케이스까지 서비스에 녹아들었고,
사용자 경험(재시도 버튼 노출)과 운영 편의성(사유 로그 관리) 모두 개선할 수 있었다.
추후에 데이터가 쌓이게 되면 이 reason을 ENUM으로 정규화해서 대시보드/통계 지표까지 확장하는 걸 다뤄보고 싶다.