[LG CNS 6기] 본 과정 33일차 TIL / [Spring AI] - OpenAI 연동, ChatClient, 프롬프트 엔지니어링

김승진·2026년 9월 15일

LG CNS AM 6기 TIL

목록 보기
42/46

1. 오늘의 한 줄 요약

OpenAI 연동과 미니프로젝트 시작 준비.

2. 오늘 배운 것

2.1 생성형 AI는 어디서 왔나

  • 뉴럴 네트워크(1940년대)
  • 딥러닝(2010년대)
  • 트랜스포머(2017년, 구글)
  • LLM(GPT, Claude)

트랜스포머는 인코더와 디코더로 이루어져 있고, GPT(Generative Pre-trained Transformer)는 그중 디코더를 쌓아서 만든 모델이라고 배웠다.

동작 방식은 패턴 학습이다. 문장 뒷부분을 지우고 다음에 올 확률이 가장 높은 단어를 고르는 식으로 답을 만든다. 그래서 모르는 것도 아는 것처럼 답을 만들어내는 환각(Hallucination) 문제가 생긴다.

2.2 프롬프트가 결과 품질을 정한다

좋은 프롬프트의 조건은 명확성, 구체성, 맥락 제공 세 가지다.
여기서 회사별 전략들이 파생되는데 오늘 나온 것만 4개이다.

  • RACE(Role, Action, Context, Expectation)
  • CARE(Context, Action, Result, Example)
  • RISE(Role, Input, Steps, Expectation)
  • CO-STAR(Context, Objective, Style, Tone, Audience, Response)

이 중 CO-STAR가 실무에서 가장 많이 쓰인다고 배웠다. 5W1H로 접근하는 방법도 있고, 기획 문서 자체를 프롬프트에 녹이는 PRD(Product Requirements Document) 방식도 있다.

GPT 공식 문서가 권장하는 프롬프트 엔지니어링 4대 전략은 이렇다.

1. 명확한 지침 작성

  • 객관적이고 수치화된 표현 쓰기
  • 페르소나 부여하기
  • XML 태그 같은 구분자로 지침과 입력을 나누기

2. 참조 텍스트 제공

  • 예시를 아예 안 주는 Zero-shot
  • 하나 주는 One-shot
  • 여러 개 주는 Few-shot

3. 하위 작업으로 분할하기
4. 생각할 시간을 부여하기

프롬프트를 쓰는 순서도 정리했다.
페르소나 → 명확한 지침 → 상세 지침 → 작성 예시 → 참고 문맥

2.3 백엔드에서 OpenAI를 부르는 두 가지 방법

백엔드(inspire_jpa)에 spring-ai-starter-model-openai 의존성이 추가됐고, application-dev.yml에 아래 설정이 붙었다.

spring:
  ai:
    openai:
      api-key: ${OPEN_AI_KEY}
      chat:
        options:
          model: ${OPEN_AI_MODEL}

OpenAiService에는 두 가지 방식이 같은 파일에 남아 있다. 하나는 주석으로 처리된 예전 방식, 다른 하나는 실제로 동작하는 방식이다.

직접 호출 (주석 처리됨) — Map으로 역할(role)과 내용(content)을 담은 메시지를 만들고, ObjectMapper로 JSON 문자열로 바꿔서 OkHttpClient로 https://api.openai.com/v1/chat/completions에 직접 POST 요청을 보낸다. 응답이 오면 objectMapper.readTree()로 트리를 만들고 node.at("/choices/0/message/content")로 실제 답변 텍스트를 꺼낸다. 응답 본문 자체는 JSON 형태의 문자열이고, 그 안의 content 자리에는 AI가 만든 또 다른 JSON 문자열이 값으로 들어가 있다. 바깥쪽을 한 번 열어 안쪽 문자열을 꺼내고, 그 안쪽 문자열을 다시 objectMapper.readValue()로 열어야 DTO에 담을 수 있는 실제 데이터가 나온다.

ChatClient (실제로 쓰는 방식)

RecommandResponseDTO result = chatClient
    .prompt()
    .system("전처리된 json 형태로만 반환해줘.")
    .user("""
        너는 날씨 전문가이고 맛있는 음식을 즐겨하는 인공지능전문가야.
        <조건>
            - 날씨 : "%s"
            - 위치 : "%s"
        </조건>
        <출력예시>
        { "weather" : "날씨", "location" : "위치", "restaurants" : [...] }
        </출력예시>
    """.formatted(weather, location))
    .call()
    .entity(RecommandResponseDTO.class);

OpenAiConfig는 ChatClient.Builder를 받아서 .build() 결과를 빈으로 등록해두는 세 줄이 전부다. 이 덕분에 다른 클래스에서는 생성자 주입만으로 chatClient를 바로 쓸 수 있다. 엔드포인트 주소를 적는 코드가 안 보이는 이유는, yml에 적어둔 키·모델 정보를 ChatClient.Builder가 읽어서 이미 채워서 내려주기 때문이다. 직접 호출에서 요청을 조립하고 보내고 파싱해서 DTO로 바꾸던 코드 네 덩어리가, .prompt().user().call().entity() 한 줄 안으로 들어가 안 보이게 됐다.

2.4 DTO로 받을지, 문자열로 받을지

같은 chatClient.prompt()...call() 뒤에 두 가지 마무리 방법이 있다.

  • .entity(DTO.class) — 항목이 나뉘어 있는 결과(맛집 추천, 퀴즈)를 자바 타입으로 바로 받는다.
  • .content() — 정해진 구조가 없는 결과(블로그 본문처럼 그냥 글)를 문자열로 받는다.

QuizResponseDTO, RecommandResponseDTO는 둘 다 @Builder, @NoArgsConstructor, @AllArgsConstructor, @JsonIgnoreProperties(ignoreUnknown = true)를 붙였고, 배열 안에 들어가는 항목(RestaurantDTO, Quiz)은 이너 스태틱 클래스로 표현했다. @JsonIgnoreProperties(ignoreUnknown = true)는 AI 응답에 DTO가 모르는 필드가 섞여 있어도 무시하고 담으라는 설정이고, 생성자 어노테이션 중 하나가 비면 Cannot construct instance ... no Creators 에러가 나면서 변환이 멈춘다.

2.5 블로그 글쓰기에 AI 붙이기

BlogController에 /ai/agent 엔드포인트가 새로 생겼고, BlogService에 contentGenerate()가 추가됐다.

public String contentGenerate(Map<String, Object> map) {
    String result = chatClient
        .prompt()
        .user("""
            넌 국문학과박사 수료한 블로그작성 전문가야.
            주어진 카테고리와 키워드를 기반으로 차분한 톤의 블로그를 작성해줘.
            글자수는 200자 이내로 작성해줘.
            <조건>
                - 카테고리 : "%s"
                - 키워드   : "%s"
            </조건>
        """.formatted((String)(map.get("category")), (String)(map.get("keyword"))))
        .call()
        .content() ;
    return result ;
}

system() 없이 user()만 있고, 출력 예시도 없다. 결과가 JSON이 아니라 그냥 글이라 .entity()가 아니라 .content()로 받는다. SecurityConfig의 화이트리스트에는 /openai/**만 추가됐고 /blogs/ai/agent는 /blogs로 시작하는 경로라 여전히 토큰이 필요하다. 같은 AI 호출인데 어느 경로에 붙였는지에 따라 인증 여부가 갈린 것이다.

2.6 에이전트, MCP

  • 에이전트: 지금 구조(Controller → Service → ChatClient → OpenAI)에서는 어느 서비스를 부를지 컨트롤러 코드가 미리 정해두는데, 에이전트를 두면 그 판단 자체를 AI에게 맡긴다. 에이전트는 판단만 하고 실제 일은 툴이 한다.
  • MCP(Model Context Protocol): AI 모델이 외부 데이터·도구·서비스와 연결될 때 쓰는 표준화된 통신 규약이다. 로컬 MCP와 원격 MCP가 있다. API는 프로그램끼리 데이터를 주고받는 인터페이스이고, MCP는 여러 API와 파일, DB를 한꺼번에 통합해서 연결하는 프로토콜이라는 점이 다르다.

2.7 RAG / LangChain

  • RAG / LangChain: LangChain은 LLM에 RAG를 결합한 프레임워크다. GPT는 학습된 시점 이후나 우리 회사 데이터는 모르기 때문에, 벡터 DB에 담아둔 자료를 검색해서 프롬프트에 얹어주는 방식이 RAG다.

3. 실습 / 적용

프론트(BlogWritePage.jsx)에 키워드 입력창과 "키워드 전송" 버튼을 추가해서, /blogs/ai/agent를 호출해 본문을 자동으로 채우는 걸 실습했다.

4. 오늘의 회고

  • 느낀 점: 오늘부터 미니프로젝트 준비 단계이다. 간단한 자기소개와 함께, 미니프로젝트의 주제를 정하는 시간이었다.
  • 다음에 할 일: 배운 내용 정리

#LGCNS #LGCNS6기 #개발자 #LGCNSINSPIRECAMP #SpringAI #OpenAI

profile
이것저것

0개의 댓글