지금 만들고있는건, 나같은 신입, 공부중인 사람들을 위한 백엔드 문제를 내주는 웹서비스이다.
private String buildPrompt(Category category) {
return String.format("""
Generate %d multiple-choice quiz questions about the topic: "%s".
Description: %s
Requirements:
- Each question must have exactly 4 options (option1 ~ option4)
- The answer field must be an integer (1, 2, 3, or 4) indicating the correct option
- Write all questions and options in Korean
- Questions should be backend developer level
- No duplicate questions
Respond ONLY with this exact JSON format (no markdown, no extra text):
{
"questions": [
{
"question": "질문 내용",
"option1": "보기 1",
"option2": "보기 2",
"option3": "보기 3",
"option4": "보기 4",
"answer": 1,
"explanation": "정답 해설"
}
]
}
""",
openAiProperties.getCountPerCategory(),
category.getName(),
category.getDescription()
);
}
문제생성은 지피티의 도움으로 프롬프트를 만들었다.
질문내용
보기 1~4
정답
설명
구조의 json 형태로 오고, 이걸 나는 DB에 저장하고 랜덤으로 유저에게 제공해준다.
우선 문제의 카테고리는
public enum CategoryStatus {
SPRING,JAVA,DATABASE,ALGORITHM
}
이렇게 4개로 구성되어있고, 이걸 for문을 돌려 각각 문제를 생성한다.
{
"generatedQuestions": [
{
"createQuestionList": {
"SPRING": [
{
"category": "SPRING",
"question": "Spring Framework에서 의존성 주입을 구현하기 위해 사용하는 어노테이션은?",
"option1": "@Autowired",
"option2": "@Component",
"option3": "@Service",
"option4": "@Repository",
"answer": 1,
"explanation": "@Autowired 어노테이션은 Spring에서 의존성 주입을 자동으로 처리하기 위해 사용됩니다."
},
{
"category": "SPRING",
"question": "Spring MVC에서 컨트롤러 클래스는 어떤 어노테이션으로 표시됩니까?",
"option1": "@Controller",
"option2": "@RestController",
"option3": "@Service",
"option4": "@Component",
"answer": 1,
"explanation": "@Controller 어노테이션은 Spring MVC에서 HTTP 요청을 처리하는 컨트롤러 클래스를 표시하는 데 사용됩니다."
},
{
"category": "SPRING",
"question": "Spring Boot에서 애플리케이션을 시작하는 주 클래스에서 사용하는 어노테이션은?",
"option1": "@SpringBootApplication",
"option2": "@EnableAutoConfiguration",
"option3": "@Configuration",
"option4": "@ComponentScan",
"answer": 1,
"explanation": "@SpringBootApplication 어노테이션은 Spring Boot 애플리케이션의 시작 클래스에 사용되며, 자동 구성 및 컴포넌트 스캔을 활성화합니다."
}
]
}
}
]
}
기존에 돌아오는 구조는 이런식이었다.
package com.example.backendquiz.infra.openai.dto;
import com.example.backendquiz.common.CategoryStatus;
import com.example.backendquiz.domain.question.Question;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.*;
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class OpenAiCreateQuestionQueueResponse {
private Queue<OpenAiCreateQuestionHashMapResponse> generatedQuestions = new ArrayDeque<>();
public void addData(OpenAiCreateQuestionHashMapResponse list) {
this.generatedQuestions.add(list);
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class OpenAiCreateQuestionHashMapResponse {
private Map<CategoryStatus, List<OpenAiCreateQuestionResponse>> createQuestionList;
public Map<CategoryStatus, List<OpenAiCreateQuestionResponse>> createListToHashMap(List<OpenAiCreateQuestionResponse> list) {
CategoryStatus status = list.stream()
.map(OpenAiCreateQuestionResponse::getCategory)
.findFirst()
.orElseThrow();
return Map.of(status, list);
}
public OpenAiCreateQuestionHashMapResponse(List<OpenAiCreateQuestionResponse> list) {
this.createQuestionList = this.createListToHashMap(list);
}
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class OpenAiCreateQuestionResponse {
private CategoryStatus category;
private String question;
private String option1;
private String option2;
private String option3;
private String option4;
private int answer;
private String explanation;
public static OpenAiCreateQuestionResponse from(Question question) {
return new OpenAiCreateQuestionResponse(
question.getCategory().getName(),
question.getQuestion(),
question.getOption1(),
question.getOption2(),
question.getOption3(),
question.getOption4(),
question.getAnswer(),
question.getExplanation()
);
}
}
}
자료구조를 공부하면서, 무슨생각인지 큐를 사용하려 했었다. 대체왜? 지금 생각해보면 이유는 생각나지 않는다. 어쨎든 저렇게 만들었고, 불필요했으며, 겹겹이 쌓인 dto 래핑은 당장 고치지 않으면 안될 것 같았다.
package com.example.backendquiz.infra.openai.dto;
import com.example.backendquiz.common.CategoryStatus;
import com.example.backendquiz.domain.question.Question;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.*;
@Getter
@AllArgsConstructor
public class OpenAiCreateQuestionMapResponse {
private Map<CategoryStatus, List<OpenAiCreateQuestionResponse>> createQuestionList;
public OpenAiCreateQuestionMapResponse() {
this.createQuestionList = new HashMap<>();
}
public void putData(List<OpenAiCreateQuestionResponse> lists) {
CategoryStatus status = lists.stream()
.map(OpenAiCreateQuestionResponse::getCategory)
.findFirst()
.orElseThrow();
this.createQuestionList.put(status, lists);
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class OpenAiCreateQuestionResponse {
private CategoryStatus category;
private String question;
private String option1;
private String option2;
private String option3;
private String option4;
private int answer;
private String explanation;
public static OpenAiCreateQuestionResponse from(Question question) {
return new OpenAiCreateQuestionResponse(
question.getCategory().getName(),
question.getQuestion(),
question.getOption1(),
question.getOption2(),
question.getOption3(),
question.getOption4(),
question.getAnswer(),
question.getExplanation()
);
}
}
}
리스트로 한번, 그 리스트를 맵 구조로 카테고리가 감싸게끔 변경했다. 여기서 CategoryStatus enum 이 NPE 를 발생시켜 Optional 로 감쌌는데, 프롬프트에 전달해주는 과정에서 문제가 생겨 로직 자체가 작동되지 않았다.
CategoryStatus status = lists.stream()
.map(OpenAiCreateQuestionResponse::getCategory)
.findFirst()
.orElseThrow();
No enum constant CategoryStatus.Optional[ALGORITHM]
기존의 optional.toString() 에서 orElseThrow() 로 바꿔 명확히 enum 타입을 잡아줬다.
{
"createQuestionList": {
"DATABASE": [
{
"category": "DATABASE",
"question": "관계형 데이터베이스에서 기본 키의 역할은 무엇인가?",
"option1": "테이블의 데이터를 삭제하는 기능",
"option2": "각 레코드를 고유하게 식별하는 기능",
"option3": "데이터베이스의 성능을 향상시키는 기능",
"option4": "데이터베이스의 백업을 관리하는 기능",
"answer": 2,
"explanation": "기본 키는 테이블의 각 레코드를 고유하게 식별하는 역할을 합니다."
},
{
"category": "DATABASE",
"question": "SQL에서 데이터를 삽입하기 위해 사용하는 명령어는 무엇인가?",
"option1": "SELECT",
"option2": "INSERT",
"option3": "UPDATE",
"option4": "DELETE",
"answer": 2,
"explanation": "데이터를 삽입하기 위해서는 INSERT 명령어를 사용합니다."
}
]
}
}
이렇게 깔끔한 json 응답이 돌아왔다.