지난 포스팅에서는 프로젝트 초기 세팅과 트러블슈팅을 다뤘습니다. 이번에는 1월 22일부터 25일까지 진행된 자소서 첨삭 기능의 백엔드 개발 전체 과정을 기록합니다.
Mock API 설계, MyBatis DB 연동, OpenAI(GPT) 실연동, 그리고 Spring AI 리팩토링까지의 여정을 상세히 담았습니다.
ResumeRequest, ResumeResponse) 설계ResumeController 구현 (POST /api/resumes)resumes 테이블 생성ResumeMapper 구현AiClient 모듈 분리 (리팩토링)429 Quota Exceeded, PromptTemplate 오류 등)먼저 클라이언트(프론트)와 어떤 데이터를 주고받을지 정의했습니다. Java 17의 record를 사용하여 불변(Immutable) 객체로 간결하게 선언했습니다.
// 요청: 사용자의 질문과 답변
public record ResumeRequest(
String question,
String answer
) {}
// 응답: AI가 분석한 점수와 피드백 리스트
public record ResumeResponse(
Map<String, Integer> score, // 4가지 평가 항목별 점수
List<Map<String, String>> feedback // 구체적인 피드백 내용
) {}
JPA 대신 SQL을 명시적으로 제어할 수 있는 MyBatis를 선택했습니다. AI 분석 요청이 들어오면, 분석 전에 원본 데이터를 DB에 먼저 저장합니다.
DB 스키마 (PostgreSQL)
CREATE TABLE resumes (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT,
question TEXT,
original_content TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
Mapper XML
(ResumeMapper.xml) 단순한 INSERT 문이지만, useGeneratedKeys="true" 옵션을 사용하여 저장된 직후 생성된 PK(id)를 바로 받아오도록 처리했습니다.
<insert id="saveResume" useGeneratedKeys="true" keyProperty="id">
INSERT INTO resumes (user_id, question, original_content)
VALUES (#{userId}, #{question}, #{content})
</insert>
가장 중요한 AI 연동 부분입니다. Spring AI의 ChatClient를 사용했습니다.
💡 프롬프트 엔지니어링 (Prompt Engineering)
단순히 "첨삭해줘"라고 하면 AI가 제멋대로 답합니다. 정확한 JSON 포맷으로 응답받기 위해 시스템 메시지를 정교하게 깎았습니다.
초기에는 ResumeService 안에 DB 저장 로직과 AI 호출 로직이 섞여 있었습니다. 단일 책임 원칙(SRP)을 지키기 위해, AI 관련 로직만 전담하는 AiClient 클래스를 별도로 분리했습니다.
ResumeService: 전체 흐름 제어 (DB 저장 호출 → AI 분석 호출)AiClient: 프롬프트 관리, OpenAI API 호출, JSON 파싱, AI 전용 예외 처리