작성일: 2026. 01. 26
한 줄 요약: API 구조를 List 형태로 리팩토링하고, AI에게 '종합 평가'와 '개별 첨삭'을 동시에 수행시키는 로직 구현
기존의 API는 한 번에 하나의 질문과 답변({q, a})만 처리할 수 있었다. 하지만 실제 취업 현장에서 자기소개서는 보통 지원 동기, 성격의 장단점, 직무 경험, 입사 후 포부 등 3~5개의 항목으로 구성된다.
사용자가 한 항목씩 요청을 보내고 기다리는 것은 UX 관점에서 좋지 않다. 따라서 오늘은 여러 개의 자기소개서 항목을 한 번에 요청받아, AI가 전체를 관통하는 종합 점수를 매기고, 각 항목별로 디테일한 수정안을 제공하는 기능을 구현하는 것이 목표다.
이 작업은 기존 API의 Request/Response 스펙을 완전히 뜯어고치는 Breaking Change이므로, 설계부터 구현까지 꼼꼼한 접근이 필요했다.
가장 먼저 클라이언트(프론트엔드)와 서버가 주고받을 데이터 규격(Protocol)을 재정의했다.
기존에는 단순 객체였으나, 이제는 유동적인 개수의 항목을 받기 위해 items라는 리스트 래퍼(Wrapper) 구조를 도입했다. 무제한 입력을 막기 위해 최대 5개까지의 제약 조건을 걸었다.
Before (AS-IS)
{
"question": "지원 동기는?",
"answer": "..."
}
After (TO-BE)
{
"items": [
{
"question": "지원 동기",
"answer": "..."
},
{
"question": "성격의 장단점",
"answer": "..."
}
]
}
응답 역시 대폭 업그레이드했다. 단순히 "잘 썼네요"라는 코멘트보다는, "이렇게 고쳐보세요"라고 구체적인 대안을 제시하는 것이 사용자에게 훨씬 유용하다. 따라서 revisedAnswer(수정된 답변) 필드를 추가했다.
또한, 점수(score)는 개별 항목 점수가 아닌, 제출된 모든 항목을 종합하여 평가한 4가지 지표로 정의했다.
설계된 내용을 바탕으로 Java 코드를 작성했다. Lombok과 Jakarta Validation을 적극 활용하여 코드를 간결하게 유지했다.
리스트 처리를 위해 내부 정적 클래스(ResumeItem)를 정의하고, @Size 어노테이션으로 비즈니스 제약 사항(1~5개)을 검증하도록 했다.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResumeRequest {
@Valid
@Size(min = 1, max = 5, message = "질문 항목은 최소 1개에서 최대 5개까지 가능합니다.")
private List<ResumeItem> items; // 리스트 구조로 변경
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class ResumeItem {
@NotBlank(message = "질문은 필수입니다.")
private String question;
@NotBlank(message = "답변 내용은 필수입니다.")
private String answer;
}
}
AI의 분석 결과는 크게 '점수(Map)'와 '결과 리스트(List)'로 나뉜다. revisedAnswer 필드를 통해 AI가 직접 첨삭(Re-write)한 내용을 담는다.
@Getter
@Builder
public class ResumeResponse {
// 4대 평가 항목 점수 (1:직무, 2:문제해결, 3:성장, 4:소통)
private Map<String, Integer> score;
// 항목별 상세 분석 및 수정안
private List<ResumeResultItem> results;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class ResumeResultItem {
private String question;
private String revisedAnswer; // AI가 보완하여 재작성한 답변
private String comment; // 수정 이유 및 조언
}
}
Controller에서 받은 요청을 처리하는 processResume 메서드도 반복문 로직으로 변경되었다.
items)을 순회하며 DB에 저장한다. (추후 히스토리 관리를 위함)AiClient에 넘겨, AI가 문맥을 파악하고 일괄 처리하도록 한다.@Override
public ResumeResponse processResume(ResumeRequest request) {
log.info("요청 수신 - 항목 수: {}", request.getItems().size());
// 1. DB 저장 (Loop 처리)
// 실제 운영 환경에서는 Batch Insert로 최적화 고려 필요
try {
for (ResumeRequest.ResumeItem item : request.getItems()) {
Map<String, Object> params = new HashMap<>();
params.put("userId", 1L); // 임시 User ID
params.put("question", item.getQuestion());
params.put("content", item.getAnswer());
resumeMapper.saveResume(params);
}
} catch (Exception e) {
log.error("DB 저장 실패 (계속 진행): ", e);
}
// 2. AI 분석 요청 (리스트 전체 전달)
return aiClient.analyzeResume(request.getItems());
}
오늘 개발의 하이라이트는 프롬프트 설계였다.
단순히 "이거 분석해줘"라고 하면, AI는 입력된 질문 개수만큼만 점수를 주거나, 형식을 멋대로 바꿀 위험이 있다.
따라서 System Prompt에 매우 강력한 제약 사항(Constraint)을 걸었다.
List<ResumeItem>을 JSON 문자열로 변환하여 주입."1", "2", ...)이 입력 순서가 아니라 카테고리 ID임을 명시했다.results 리스트는 입력된 질문의 개수(N)와 정확히 일치하도록 지시했다.[실제 적용된 프롬프트 일부]
[작업 1: 종합 평가 점수 산정]
제출된 모든 항목을 통틀어, 지원자의 역량을 아래 4가지 기준으로 평가하여 100점 만점으로 점수를 매기세요.
**주의: 입력된 항목의 개수와 상관없이, 반드시 아래 4가지 카테고리 각각에 대한 점수를 산출해야 합니다.**
- 카테고리 1: 직무 적합성 (Java 백엔드 개발자 기준)
- 카테고리 2: 문제 해결력
- 카테고리 3: 성장 가능성
- 카테고리 4: 의사소통
[작업 2: 항목별 상세 첨삭]
**사용자가 입력한 질문/답변이 N개라면, results 리스트에도 정확히 N개의 첨삭 결과가 있어야 합니다.**
순서는 입력된 순서를 유지하세요.
이 프롬프트 덕분에 AI는 질문이 몇 개가 들어오든 헷갈리지 않고 '종합 점수 4개'와 '개별 첨삭 N개'를 정확한 JSON 포맷으로 반환하게 되었다.
이번 변경은 API 스펙이 변하는 중요한 작업이었기 때문에 develop 브랜치에서 직접 작업하지 않고 Feature Branch 전략을 사용했다.
feature/resume-api-update 브랜치 생성develop 브랜치로 Merge이 과정을 통해 운영 중인 코드에 영향을 주지 않고 안전하게 대규모 리팩토링을 마칠 수 있었다.
오늘 작업을 통해 SelfCheck AI는 단순한 '검사기'를 넘어, 실제 입사 지원 시나리오에 맞는 '종합 컨설턴트'의 모습을 갖추게 되었다.
특히 List 형태의 데이터를 다루면서 AI가 문맥(Context)을 놓치지 않도록 프롬프트를 정교하게 다듬는 과정에서 많은 것을 배웠다. 다음 단계는 이 프롬프트를 더 고도화하여, 지원자의 답변 스타일(톤앤매너)까지 분석하는 기능을 고민해봐야겠다.