작성일: 2026. 01. 26
한 줄 요약: API 구조 변경 시 겪은 JSON 매핑 오류와, AI가 점수 키(Key)를 헷갈리는 문제를 프롬프트로 해결한 과정
오늘 SelfCheck AI 서비스의 핵심 기능을 '단일 항목 첨삭'에서 '다중 항목 일괄 첨삭'으로 업그레이드하는 작업을 진행했다.
코드만 고치면 금방 끝날 줄 알았지만, 예상치 못한 곳에서 500 에러(NPE)가 터졌고, AI는 내 의도와 다르게 동문서답을 했다.
오늘 겪은 두 가지의 굵직한 에러와 그 해결 과정을 기록으로 남긴다.
API 리팩토링 후 서버를 띄우고 Postman으로 테스트 요청을 보내자마자, 콘솔에 빨간색 에러 로그가 가득 찼다.
java.lang.NullPointerException: Cannot invoke "java.util.List.size()" because the return value of "selfcheck_ai.resume.dto.ResumeRequest.getItems()" is null
at selfcheck_ai.resume.service.ResumeServiceImpl.processResume(ResumeServiceImpl.java:25)
at selfcheck_ai.resume.controller.ResumeController.createResume(ResumeController.java:28)
서비스 로직인 request.getItems().size() 부분에서 items가 null이라며 죽어버린 것.
가설 1: DTO에 Setter가 없나?
가장 먼저 의심한 것은 Lombok 설정이었다. items 필드에 값이 주입되지 않았으니, @Getter만 있고 @Setter나 @Data가 없어서 Jackson 라이브러리가 값을 못 넣은 게 아닐까?
// ResumeRequest.java 확인
@Data // @Getter, @Setter, @RequiredArgsConstructor 등 포함
public class ResumeRequest {
private List<ResumeItem> items;
...
}
확인 결과 @Data가 잘 붙어 있었다. DTO 코드에는 문제가 없었다.
가설 2: 요청 데이터(Payload)가 잘못되었나?
로그를 다시 보니, 내가 보내고 있는 JSON 데이터의 모양이 문제였다.
백엔드 코드는 items라는 리스트를 받도록 수정했는데, 정작 요청은 구버전 API 스펙 그대로 보내고 있었던 것이다.
❌ 내가 보낸 요청 (Wrong)
{
"question": "지원 동기는?",
"answer": "..."
}
백엔드는 items라는 키를 기다리고 있는데, 엉뚱한 question 키가 들어오니 매핑을 못 하고 items를 null로 둔 채 넘어가 버린 것이다.
Step 1: 요청 JSON 수정
API 스펙에 맞춰 JSON 구조를 변경했다.
✅ 수정된 요청 (Correct)
{
"items": [
{ "question": "지원 동기는?", "answer": "..." },
{ "question": "성격 장단점은?", "answer": "..." }
]
}
Step 2: 예외 처리 강화
단순히 클라이언트가 잘 보내길 기대하기보다, JSON 변환 과정에서 문제가 생기면 명확한 에러 메시지를 뱉도록 ErrorCode를 추가했다.
// AiClient.java
try {
userMessageText = objectMapper.writeValueAsString(items);
} catch (JsonProcessingException e) {
throw new BusinessException(ErrorCode.JSON_PROCESSING_ERROR);
}
NPE를 해결하고 드디어 AI가 응답을 줬다. 그런데 응답 내용을 뜯어보니 이상했다.
분명히 평가 기준은 4가지(직무 적합성, 문제 해결력, 성장 가능성, 의사소통)로 정해두었는데, 질문을 2개(items: [Q1, Q2]) 보냈더니 점수도 2개만 왔다.
😱 AI의 이상한 응답
"score": {
"1": 60, // ??? 직무 적합성 점수여야 하는데...
"2": 70, // ??? 문제 해결력 점수여야 하는데...
"3": 0, // 0점?
"4": 0 // 0점?
}
심지어 3번, 4번 항목은 아예 평가를 안 했다.
"AI는 맥락(Context)을 오해했다."
프롬프트에서 "1": 점수, "2": 점수 라고 JSON 예시를 보여줬더니, AI는 이 숫자를 '평가 카테고리 번호'가 아니라 '입력된 질문의 순서(Index)'로 해석해버린 것이다.
"1": 점수 생성"1": 점수, "2": 점수 생성이것은 전형적인 Overfitting(과적합) 현상이다. 프롬프트가 모호하면 AI는 가장 쉬운 방법(입력 개수 = 출력 개수)으로 추론해버린다.
AI에게 "Strong Typing"을 적용했다. 개발자가 코드에 타입을 명시하듯, 프롬프트에도 각 숫자가 무엇을 의미하는지 '절대 규칙'을 박아넣었다.
수정된 프롬프트 (AiClient.java)
String systemMessageText = """
...
[작업 1: 종합 평가]
**주의: 입력된 항목의 개수와 상관없이, 반드시 아래 4가지 카테고리 각각에 대한 점수를 산출해야 합니다.**
[출력 형식 가이드]
JSON의 Key "1", "2", "3", "4"는 입력 순서가 아닙니다.
아래 정의된 **평가 카테고리의 고유 ID**입니다.
- "1": 직무 적합성
- "2": 문제 해결력
...
[작업 2: 상세 첨삭]
사용자가 입력한 질문이 N개라면, results 리스트도 정확히 N개를 반환하세요.
""";
✅ 결과 (Correct Response)
이제 질문을 2개 보내든 5개 보내든, score 객체는 항상 4개의 평가 기준을 꽉 채워서 보내주고, results 리스트는 입력 개수에 맞춰서 정확하게 반환하게 되었다.
NPE가 났을 때 코드를 의심하기 전에 "내가 무엇을 입력했는가"를 먼저 확인했어야 했다. DTO 구조를 바꿨다면 Payload 구조도 당연히 바꿨어야 했는데, 관성적으로 테스트하다가 시간을 허비했다.
AI에게 "적당히 잘 해줘"라고 하면 "적당히 엉망으로" 해준다.
이렇게 유치원생에게 설명하듯(혹은 컴파일러에게 타입 힌트를 주듯) 아주 구체적이고 명확한 제약 조건을 걸어야 원하는 퀄리티의 결과를 얻을 수 있음을 깨달았다. Prompt Engineering은 이제 선택이 아니라 필수 역량이다.