Spring AI 프롬프트 엔지니어링

한소연·2026년 3월 24일

내일배움캠프

목록 보기
14/15
post-thumbnail

LLM이 정확하게 답변하도록 입력을 정교하게 설계하는 기술.
PromptTemplate부터 구조화된 출력(Structured Output)까지, 실전 코드와 함께 정리합니다.


목차

  1. 프롬프트 엔지니어링이란
  2. PromptTemplate
  3. 복수 메시지 구성
  4. Default 메시지 & 옵션
  5. 프롬프트 기법 6가지
  6. 구조화된 출력 (Structured Output)

1. 프롬프트 엔지니어링이란

프롬프트(Prompt) 는 LLM이 사용자의 의도를 정확히 파악하여 답변하도록 돕는 구체적인 명령문이다. LLM은 프롬프트를 입력받아 학습된 패턴과 지식을 기반으로 최적의 결과물을 생성한다.

프롬프트의 3대 구성 요소

요소설명예시
상황 (Context)어떤 배경이나 환경에서 작업하는가"당신은 시니어 Spring 개발자입니다."
내용 (Task)구체적으로 무엇을 수행해야 하는가"다음 코드의 문제점을 분석해줘."
형식 (Format)어떤 구조나 스타일로 응답해야 하는가"JSON 형식으로, 항목별로 나누어 설명해줘."

프롬프트 엔지니어링 기본 원칙

원칙설명
명확하고 구체적인 요청모호함 없이 답변의 범위와 방향을 명확히 정의
배경 정보 제공LLM이 의도를 파악할 수 있도록 상세한 맥락 함께 제공
간결한 문장 사용수식어가 많은 복잡한 문장보다 직관적인 문장 선호
적절한 예시 사용원하는 스타일과 형식을 구체적 예시로 제시
다단계 질문 피하기하나의 프롬프트에는 하나의 질문만 담기
역할 부여하기특정 작업에 집중하도록 모델에게 명확한 역할 부여

2. PromptTemplate

PromptTemplate은 정적인 텍스트와 동적인 데이터를 결합하여 LLM이 이해할 수 있는 최종 프롬프트를 만드는 설계도 역할을 한다. {variable} 형태의 자리 표시자(Placeholder)로 실행 시점에 데이터를 주입할 수 있다.

PromptTemplate vs SystemPromptTemplate

구분PromptTemplateSystemPromptTemplate
역할사용자의 질문(User Message) 구조화AI의 페르소나·지침(System Message) 설정
메시지 타입UserMessage 생성SystemMessage 생성
사용 사례"분야가 {subject}인 뉴스 요약해줘""당신은 {language} 전문 번역가입니다."

주요 메서드

메서드역할
create()바인딩된 데이터로 최종 Prompt 객체 생성
render(Map<String, Object>)자리 표시자에 데이터를 채운 최종 문자열 반환
template(String)사용할 프롬프트 원본 텍스트 설정

사용 예시: PromptTemplate으로 동적 질문 생성

방법 1 — render()String으로 호출

@Test
void usePromptTemplateTest() {
    // 1. 템플릿 정의
    String templateString = "{person}의 명언 3개를 한국어로 출력해줘";
    PromptTemplate promptTemplate = PromptTemplate.builder()
            .template(templateString)
            .build();

    // 2. 데이터 바인딩 및 렌더링
    String renderedContent = promptTemplate.render(Map.of("person", "스티브잡스"));

    // 3. ChatClient 호출
    String answer = chatClient.prompt(renderedContent)
            .call()
            .content();
}

방법 2 — create()Prompt 객체로 호출

@Test
void usePromptTemplate2Test() {
    PromptTemplate promptTemplate = PromptTemplate.builder()
            .template("{person}의 명언 3개를 한국어로 출력해줘")
            .build();

    // create()로 Prompt 객체 생성
    Prompt prompt = promptTemplate.create(Map.of("person", "스티브잡스"));

    String answer = chatClient.prompt(prompt)
            .call()
            .content();
}

응답 예시

1) "항상 배고프고, 항상 어리석게 있어라." — 스티브 잡스
2) "혁신은 리더와 추종자를 구분한다." — 스티브 잡스
3) "당신의 시간은 한정되어 있으니, 다른 사람의 삶을 사느라 낭비하지 마라." — 스티브 잡스

사용 예시: SystemPromptTemplate으로 역할 부여

@Test
void useSystemPromptTest() {
    String message = "스프링 DI에 대해 설명해줘";

    SystemPromptTemplate promptTemplate = SystemPromptTemplate.builder()
            .template("당신은 이제부터 {role} 전문가입니다. 전문 용어를 섞어서 답변하세요.")
            .build();

    Prompt prompt = promptTemplate.create(Map.of("role", "스프링"));

    String answer = chatClient.prompt(prompt)
            .user(message)
            .call()
            .content();
}

3. 복수 메시지 구성

LLM에 전달되는 프롬프트는 단순 질문 하나가 아니라 대화 히스토리를 포함하는 리스트 구조다.

메시지 타입

타입역할
SystemMessageAI의 역할, 제약 사항, 답변 형식 정의
UserMessage사용자의 질문이나 명령 (이력 포함 가능)
AssistantMessage이전 AI 답변. 포함하면 대화 맥락을 기억하는 것처럼 동작

주요 활용 사례

  • Few-shot Prompting: 원하는 답변 형식을 User-Assistant 쌍으로 예시 제공
  • 대화 이력 유지: 이전 대화 내용을 누적하여 현재 질문에 반영
  • 복합 지시사항: 여러 단계의 지시를 시간 순서대로 나열

사용 예시

@Test
void test() {
    List<Message> messageList = new ArrayList<>();

    // 1. 시스템 페르소나 설정
    messageList.add(new SystemMessage("당신은 친절한 요리사입니다. 사용자의 질문에 요리 비유를 들어 답변하세요."));

    // 2. 이전 대화 맥락 추가 (Few-shot)
    messageList.add(new UserMessage("안녕? 첫 번째 질문이야. 오늘 날씨 어때?"));
    messageList.add(new AssistantMessage("안녕하세요! 오늘은 신선한 샐러드처럼 상큼한 날씨네요."));

    // 3. 현재 질문 추가
    messageList.add(new UserMessage("그럼 이런 날씨엔 어떤 공부를 하면 좋을까?"));

    String answer = chatClient.prompt()
            .messages(messageList)
            .call()
            .content();
}

💡 messages() 메서드에 List<Message>를 통째로 전달하면 순서대로 대화 맥락이 구성된다.


4. Default 메시지 & 옵션

ChatClient.Builderdefault* 메서드를 사용하면 애플리케이션 전역에서 공통으로 적용될 속성을 정의할 수 있다.

주요 메서드

메서드설명주요 용도
defaultSystem()기본 System Message 설정AI 역할, 답변 스타일, 제약 사항 전역 정의
defaultUser()기본 User Message 설정공통 질문 접두어·형식 지정
defaultOptions()기본 ChatOptions 설정Temperature, 모델명, 최대 토큰 수 일괄 적용

사용 예시: 어린 왕자 챗봇

@Configuration
public class LittlePrinceConfig {

    @Bean
    public ChatClient littlePrinceChatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("""
                    너는 '어린 왕자'.
                    지구에서 만난 친구들에게 너의 별 B612와 장미, 그리고 여우에 대해 이야기해줘.
                    말투는 항상 '~야', '~해' 같은 다정한 반말을 사용해줘.
                    """)
                .defaultOptions(ChatOptions.builder()
                        .temperature(0.7)
                        .maxTokens(800)
                        .build())
                .build();
    }
}
@Service
public class LittlePrinceService {

    private final ChatClient chatClient;

    // 기본 설정 그대로 사용
    public String chat(String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }

    // 개인화 대화 — 기존 클라이언트 복제 후 일부 변경
    public String personalizedChat(String userName, String message) {
        return ChatClient.builder(chatClient.mutate())
                .defaultSystem(s -> s.replace("너는 '어린 왕자'야.",
                                              "너는 '어린 왕자'고, 지금 대화하는 친구 이름은 {name}이야."))
                .build()
                .prompt()
                .system(s -> s.param("name", userName))
                .user(message)
                .call()
                .content();
    }
}

5. 프롬프트 기법 6가지

5-1. 제로-샷 프롬프트 (Zero-Shot)

사전 예시 없이 지시사항만으로 작업을 수행하게 하는 기법. LLM이 학습 과정에서 습득한 광범위한 지식을 활용한다.

  • 장점: 구성이 간단하고 토큰 소모 최소화
  • 적합한 작업: 번역, 요약, 감정 분석, 주제 분류 등 명확한 작업
  • 한계: 특수 도메인이나 복잡한 비즈니스 로직에서 정확도 저하 가능
@Test
void test() {
    String text = "오늘은 영하 20도로 피부가 아플 정도로 추워";
    PromptTemplate promptTemplate = PromptTemplate.builder()
            .template("""
                다음 텍스트의 감정을 [긍정, 부정, 중립] 중 하나로 분류하세요.
                레이블만 반환하세요.
                텍스트: {input}
                """)
            .build();

    String sentiment = chatClient.prompt()
            .user(promptTemplate.render(Map.of("input", text)))
            .call()
            .content();
    // 출력: 부정
}

💡 감정 분류처럼 응답이 범주화된 경우 maxTokens(4)로 제한하면 불필요한 설명을 방지하고 비용을 절감할 수 있다.


5-2. 퓨-샷 프롬프트 (Few-Shot)

지시사항과 함께 예시 데이터를 제공하여 모델이 문맥 내에서 학습하게 하는 기법.

구분특징
정확도 향상복잡한 규칙이나 톤앤매너를 예시로 직접 학습
구조화된 출력JSON, CSV 등 특정 형식을 예시로 보장
일관성 유지여러 번 호출해도 동일 형식·스타일 유지

구성: SystemMessage(지침) + User/Assistant Pairs(예시) + UserMessage(실제 질문)

@Test
void test() {
    String reviewText = "이 상품은 색상도 맘에 들고 기능도 좋아요. 정말 맘에 듭니다.";

    String answer = chatClient.prompt()
            .messages(
                    // 1. 지침 (System Message)
                    new SystemMessage("당신은 리뷰 감정 분석가입니다. 응답은 반드시 JSON 형식으로 'sentiment'와 'confidence'를 포함해야 합니다."),

                    // 2. 예시 1
                    new UserMessage("이 제품 정말 최고예요! 배송도 빨랐습니다."),
                    new AssistantMessage("{\"sentiment\": \"positive\", \"confidence\": 0.98}"),

                    // 3. 예시 2
                    new UserMessage("생각보다 별로네요. 디자인은 예쁜데 품질이 떨어집니다."),
                    new AssistantMessage("{\"sentiment\": \"negative\", \"confidence\": 0.85}"),

                    // 4. 실제 질문
                    new UserMessage(reviewText)
            )
            .call()
            .content();
    // 출력: {"sentiment": "positive", "confidence": 0.95}
}

💡 AI가 답변한 것처럼 AssistantMessage를 미리 넣어주는 것이 퓨-샷의 핵심이다. 모델은 이를 보고 응답 형식을 그대로 따른다.


5-3. 역할 부여 프롬프트 (Role Prompting)

SystemMessage를 통해 LLM에게 특정 페르소나를 주입하는 기법.

  • 전문성 강화: "데이터 과학자" 역할 → 해당 분야의 전문 용어와 논리 구조 활용
  • 톤 조절: "유치원 선생님" → 친절하고 쉽게, "셰익스피어" → 고전적 문체
  • 맥락 제한: 특정 관점에서만 답변하도록 범위 제한
public String getPersonalizedResponse(String role, String userInput) {
    return this.chatClient.mutate()
            .build()
            .prompt()
            .system(sp -> sp
                    .text("당신은 {role}입니다. 해당 역할의 전문성과 문체를 사용하여 답변하세요.")
                    .param("role", role))
            .user(userInput)
            .call()
            .content();
}

// 호출: getPersonalizedResponse("30년 경력의 프랑스 요리 쉐프", "크리스마스에 가족과 즐길 수 있는 요리 추천해줘")

5-4. 스텝-백 프롬프트 (Step-Back Prompting)

구체적인 질문 → 추상적인 배경 원리로 후퇴(Step-back) 한 후 그 지식을 바탕으로 최종 답변을 도출하는 기법.

3단계 프로세스

1. Step-back Question 생성
   원래 질문에서 핵심 개념을 추출해 더 넓은 범위의 질문 생성

2. 배경 지식 확보
   스텝-백 질문으로 일반 원리·역사적 배경을 먼저 확보

3. 최종 답변 생성
   원래 질문 + 배경 지식을 결합해 구체적 결론 도출
public String getStepBackAnswer(String originalQuestion) {

    // 1단계: Step-back 질문 생성
    String stepBackQuestion = chatClient.prompt()
            .user(u -> u.text("다음 질문에서 핵심적인 원리나 배경을 묻는 더 포괄적인 질문 하나만 생성해줘. 질문: {question}")
                    .param("question", originalQuestion))
            .call()
            .content();

    // 2단계: 배경 지식 확보
    String backgroundKnowledge = chatClient.prompt()
            .user(stepBackQuestion)
            .call()
            .content();

    // 3단계: 최종 답변 생성
    return chatClient.prompt()
            .system("너는 제공된 배경 지식을 바탕으로 사용자의 질문에 논리적으로 답변하는 전문가야.")
            .user(u -> u.text("""
                    [배경 지식]: {knowledge}
                    [사용자 질문]: {question}
                    
                    위 배경 지식을 참고하여 질문에 대해 구체적이고 정확하게 답변해줘.
                    """)
                    .param("knowledge", backgroundKnowledge)
                    .param("question", originalQuestion))
            .call()
            .content();
}

실행 예시

[원래 질문] 우리 집 냉장고 문이 잘 안 닫히는데 어떻게 고쳐야 해?

[Step-back 질문]
냉장고 문이 잘 닫히지 않는 문제의 근본 원인(문 패킹·경첩·문 틈·기기 수평·내부 압력 등)은
무엇이고, 이를 진단·예방·수리할 때 적용되는 기본 원리와 점검 순서는 무엇인가?

[최종 답변]
가장 흔한 원인은 문고무(가스켓) 오염·손상 또는 문 정렬 문제입니다.
아래 순서대로 점검해 보세요...

💡 RAG 시스템과 결합 시 스텝-백 질문으로 더 광범위한 문서를 검색할 수 있어 검색 품질이 향상된다.


5-5. 생각의 사슬 프롬프트 (Chain of Thought, CoT)

LLM이 결과만 내놓는 것이 아니라 중간 논리 과정을 스스로 나열하게 만드는 기법.

종류설명
Zero-Shot CoT"단계별로 생각해 보자(Let's think step by step)" 문구만으로 추론 능력 활성화
Few-Shot CoT풀이 과정이 포함된 예시를 몇 개 제공하여 논리 전개 방식 학습
public String solveComplexProblem(String problem) {
    return this.chatClient.prompt()
            .system("""
                    당신은 논리적인 분석가입니다.
                    문제를 해결할 때 반드시 다음 단계를 준수하세요:
                    1. 주어진 정보를 분석합니다.
                    2. 단계별로 추론 과정을 나열합니다.
                    3. 최종 결론을 도출합니다.
                    """)
            .user(u -> u.text("""
                    문제: {problem}
                    
                    도움말: 한 걸음씩 차근차근 생각해 봅시다.
                    """)
                    .param("problem", problem))
            .call()
            .content();
}

실행 예시

[질문] A는 B보다 크고 B는 C보다 작아. 누가 제일 커?

[CoT 답변]
1) 주어진 정보 분석
   - A > B, C > B

2) 단계별 추론
   - A와 C 사이의 관계는 주어지지 않음
   - A=5, B=3, C=4 이면 A가 제일 큼
   - A=5, B=3, C=7 이면 C가 제일 큼

3) 최종 결론
   주어진 정보만으로 누가 제일 큰지 결정할 수 없다.

💡 temperature를 낮게(0.3~0.5) 설정하면 엉뚱한 방향으로 추론(Hallucination)하는 것을 방지하고 더 일관된 논리 구조를 유지할 수 있다.


5-6. 자기 일관성 (Self-Consistency)

동일한 프롬프트를 여러 번 병렬로 요청하고 가장 많이 중복된 답변(다수결) 을 최종 결과로 선택하는 기법.

동일 프롬프트 → N회 요청 → 다양한 추론 경로 → 다수결(Majority Voting) → 최종 답변
  • temperature를 약간 높게(0.7) 설정하여 응답 다양성 확보
  • 환각(Hallucination) 감소 효과
  • 수학, 논리 문제에서 정확도를 획기적으로 향상
public String getConsistentSentiment(String review) {
    int sampleCount = 5;

    // 1. 5회 요청 → 응답 리스트 생성
    List<String> votes = IntStream.range(0, sampleCount)
            .mapToObj(i -> fetchSentiment(review))
            .toList();

    // 2. 다수결 집계
    return votes.stream()
            .collect(Collectors.groupingBy(s -> s, Collectors.counting()))
            .entrySet().stream()
            .max(Map.Entry.comparingByValue())
            .map(Map.Entry::getKey)
            .orElse("판단 불가");
}

private String fetchSentiment(String review) {
    return chatClient.prompt()
            .system("""
                    당신은 리뷰 분석가입니다.
                    응답은 반드시 [긍정, 부정]중 하나로 분류하세요.
                    레이블만 반환하세요.
                    """)
            .user(review)
            .options(ChatOptions.builder()
                    .model("gpt-4o-mini")
                    .temperature(0.7)
                    .build())
            .call()
            .content();
}

6. 구조화된 출력 (Structured Output)

LLM은 기본적으로 텍스트를 생성하지만, 개발자는 파싱 가능한 데이터가 필요하다. Spring AI는 이 과정을 자동화한다.

형식 지침 생성 → LLM 호출 → JSON 추출 → Java 객체 역직렬화

StructuredOutputConverter 인터페이스

public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {
    // FormatProvider: LLM에게 전달할 출력 형식 지침 생성
    // Converter:      LLM 응답(String)을 Java 객체(T)로 변환
}

주요 구현체

구현체설명
BeanOutputConverterJava Bean(POJO) 기반 JSON 생성 및 변환. 가장 범용적
MapOutputConverterMap<String, Object> 형태로 변환
ListOutputConverterList<String> 형태로 변환

사용 방식 비교

방식설명
저수준converter.getFormat()으로 지침 직접 삽입 → converter.convert(response)로 변환
고수준ChatClient.call().entity(Class<T>)로 내부 자동 처리

6-1. ListOutputConverter

LLM 응답을 List<String>으로 변환한다. 쉼표(,)로 구분된 리스트 형식을 지침으로 생성한다.

저수준 방식

@Test
void lowLevelTest() {
    // 1. 컨버터 초기화
    ListOutputConverter converter = new ListOutputConverter();

    // 2. {format} 플레이스홀더 포함 템플릿 작성
    PromptTemplate promptTemplate = PromptTemplate.builder()
            .template("인천에서 유명한 관광지 목록 5개를 출력하세요. {format}")
            .build();

    // 3. 포맷 지침 바인딩
    Prompt prompt = promptTemplate.create(Map.of("format", converter.getFormat()));

    String response = chatClient.prompt(prompt).call().content();

    // 4. String → List<String> 변환
    List<String> locations = converter.convert(response);
    // 출력: [인천 차이나타운, 월미도, 송도 센트럴파크, 을왕리 해수욕장, 소래포구]
}

고수준 방식

@Test
void highLevelTest() {
    List<String> locations = chatClient.prompt()
            .user("인천에서 유명한 관광지 목록 5개를 출력하세요.")
            .call()
            .entity(new ListOutputConverter());  // 자동 변환
    // 출력: [인천 차이나타운, 월미도, 송도 센트럴파크, 인천대공원, 을왕리 해수욕장]
}

6-2. BeanOutputConverter

LLM 응답을 Java POJO(Record 포함)로 변환한다. 내부적으로 Jackson으로 JSON을 객체에 매핑한다.

공통 DTO 정의

public record University(String city, List<String> names) {}

단일 객체 변환 — 저수준

@Test
void lowLevelTest() {
    BeanOutputConverter<University> converter = new BeanOutputConverter<>(University.class);
    PromptTemplate promptTemplate = PromptTemplate.builder()
            .template("{city}의 대학교 이름 5개를 출력하세요. {format}")
            .build();

    Prompt prompt = promptTemplate.create(Map.of("city", "인천", "format", converter.getFormat()));
    String response = chatClient.prompt(prompt).call().content();

    University university = converter.convert(response);
    // 출력: University[city=인천, names=[인하대학교, 인천대학교, 경인교육대학교, 연세대학교 국제캠퍼스, 가천대학교]]
}

단일 객체 변환 — 고수준

@Test
void highLevelTest() {
    University university = chatClient.prompt()
            .user("%s의 대학교 이름 5개를 출력하세요.".formatted("인천"))
            .call()
            .entity(University.class);
    // 출력: University[city=인천, names=[인하대학교, 인천대학교, ...]]
}

리스트 변환 — ParameterizedTypeReference 활용

@Test
void lowLevelTest() {
    // List<T> 변환 시 ParameterizedTypeReference 필수
    BeanOutputConverter<List<University>> converter =
            new BeanOutputConverter<>(new ParameterizedTypeReference<>() {});

    PromptTemplate promptTemplate = PromptTemplate.builder()
            .template("""
                다음 도시들의 대학교 목록 5개를 출력하세요.
                도시: {cities}
                {format}
                """)
            .build();

    Prompt prompt = promptTemplate.create(Map.of("cities", "서울,인천,부산", "format", converter.getFormat()));
    String response = chatClient.prompt(prompt).call().content();

    List<University> universities = converter.convert(response);
    // 출력: [University[city=서울, names=[서울대학교, 연세대학교, ...]], ...]
}

6-3. MapOutputConverter

LLM 응답을 Map<String, Object> 형태로 변환한다. 별도 클래스 없이 동적 구조의 데이터를 처리할 때 유용하다.

@Test
void highLevelTest() {
    String template = "{university}대학교의 정보를 출력하세요.";

    Map<String, Object> information = chatClient.prompt()
            .user(u -> u.text(template).param("university", "서울"))
            .call()
            .entity(new MapOutputConverter());

    // 출력: {established=1946, name_en=Seoul National University, students={...}, ...}
}

6-4. 시스템 메시지와 결합으로 정확도 향상

entity()의 출력 형식 지침(사용자 메시지)과 시스템 메시지를 분리하면 더 높은 정확도를 얻을 수 있다.

구분위치역할
시스템 메시지전역 설정역할 정의, 분석 가이드라인, Few-shot 예시
entity() 지침사용자 메시지 하단JSON 구조 정의, 타입 매핑
public record TextClassification(String text, Sentiment classification) {
    public enum Sentiment { POSITIVE, NEUTRAL, NEGATIVE }
}
@Test
void test() {
    String text = "오늘은 날씨가 매우 추워 외출하기 싫어. 비가 오면 시원하고 좋은데, 좋은 건가 나쁜건가?";

    TextClassification classification = chatClient.prompt()
            // 1. 분석 로직 + 페르소나 (시스템 메시지)
            .system("""
                    당신은 텍스트의 감정을 분석하는 전문가입니다.
                    입력된 문장의 뉘앙스를 살펴 [POSITIVE, NEUTRAL, NEGATIVE] 중 하나로 분류하세요.
                    - 단어뿐만 아니라 문맥의 흐름을 파악하십시오.
                    - 확실하지 않은 경우 NEUTRAL로 분류하십시오.
                    - 유효한 JSON으로 반환하십시오.
                    """)
            // 2. 사용자 메시지
            .user(u -> u.text("텍스트: {text}").param("text", text))
            .call()
            // 3. 자동 변환 (BeanOutputConverter가 내부적으로 JSON 구조 지침 추가)
            .entity(TextClassification.class);

    // 출력: TextClassification[text=오늘은..., classification=NEUTRAL]
}

정리

기법핵심적합한 상황
Zero-Shot예시 없이 지시만으로 수행번역, 요약 등 일반적 작업
Few-Shot예시 데이터로 형식·규칙 학습특정 출력 형식, 도메인 특화 분류
Role Prompting페르소나 주입으로 전문성 강화전문가 답변, 특정 문체 적용
Step-Back배경 원리 먼저 파악 후 답변복잡한 진단, 근본 원인 분석
CoT중간 추론 과정을 나열수학, 논리 문제, 다단계 추론
Self-Consistency다수결로 최종 답변 선택환각 감소, 신뢰성이 중요한 분류

구조화된 출력은 단순히 형식을 강제하는 것이 아니라, LLM의 응답을 신뢰 가능한 데이터로 만드는 과정이다. 시스템 메시지(비즈니스 로직)와 entity()(구조 정의)를 분리하는 것이 핵심이다.


Copyright ⓒ TeamSparta · 기술 강의노트를 바탕으로 제작된 블로그입니다.

profile
안 되면 될 때까지

0개의 댓글