[TIL] 개인프로젝트 DTO 구조

코딩냥이·2026년 3월 17일

지금 만들고있는건, 나같은 신입, 공부중인 사람들을 위한 백엔드 문제를 내주는 웹서비스이다.

문제생성

    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 애플리케이션의 시작 클래스에 사용되며, 자동 구성 및 컴포넌트 스캔을 활성화합니다."
            }
          ]
        }
      }
    ]
  }

기존에 돌아오는 구조는 이런식이었다.

기존 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
@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 래핑은 당장 고치지 않으면 안될 것 같았다.

리팩토링된 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 응답이 돌아왔다.

profile
코딩하는 냥이입니다.

0개의 댓글