Gemini AI API 연동

배지원·2025년 1월 17일

Project

목록 보기
2/2

이번에 프로젝트를 진행하다가 AI를 연동하여 기능을 구현해보면 어떨까라는 생각을 가지게 되어 Google에서 제공하는 Gemini AI를 연동해보기로 하였다.

1. 인증 키 발급

Gemini API를 사용하기 위해서는 인증 키가 필요한다. 따라서 Google Gemini 사이트에 가서 발급받을 수 있다.

Google Gemini AI 사이트

  • Get API key를 통해 key를 발급받고 해당 키값을 메모해 둔다.

2. API 구조 분석

외부 API와 통신하기 위해서는 Request / Response의 구조를 파악해야 한다.

(1) API URL

curl \
  -H 'Content-Type: application/json' \
  -d '{"contents":[{"parts":[{"text":"Explain how AI works"}]}]}' \
  -X POST 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=YOUR_API_KEY'
  • API URL은 다음과 같이 넘어가게 된다. 여기서 YOUR_API_KEY에 이전에 받은 API KEY값을 넣어주면 되는데, 그냥 넣게 되면 외부에 노출이 될 수 있으므로 환경변수를 통해 숨겨놓기로 한다.

(2) Request

{
  "contents": [{
    "parts":[{
      "text": "서울 맛집을 추천해줘"
    }]
  }]
}
  • Request에는 content안에 parts안에 text를 통해 요청받고 있다.
    이때 text에는 AI에게 질문하고 싶은 질문내용을 적으면 된다.

(3) Response

{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "text": "**전통 한국 요리**\n
          }
        ],
        "role": "model"
      },
      "finishReason": "STOP",
      "index": 0,
      "safetyRatings": [
        {
          "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
          "probability": "NEGLIGIBLE"
        },
        {
          "category": "HARM_CATEGORY_HATE_SPEECH",
          "probability": "NEGLIGIBLE"
        },
        {
          "category": "HARM_CATEGORY_HARASSMENT",
          "probability": "NEGLIGIBLE"
        },
        {
          "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
          "probability": "NEGLIGIBLE"
        }
      ]
    }
  ],
  "promptFeedback": {
    "safetyRatings": [
      {
        "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
        "probability": "NEGLIGIBLE"
      },
      {
        "category": "HARM_CATEGORY_HATE_SPEECH",
        "probability": "NEGLIGIBLE"
      },
      {
        "category": "HARM_CATEGORY_HARASSMENT",
        "probability": "NEGLIGIBLE"
      },
      {
        "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
        "probability": "NEGLIGIBLE"
      }
    ]
  }
}
  • 해당 요청일 들어오면 AI API를 통해 Gemini에서 조회를 하고 해당 값을 반환해 준다.
    이때 우리는 text 부분만 필요하므로
    candidates
    └ content
     └ parts
       └ text
    부분만 구성하여 받아올 수 있도록 하겠다.

3. 실습

통신하기 위해서는 여러 방식중에 HttpInterface를 사용하도록 하겠다.

1. HTTPInterface란?

HTTP 인터페이스는 웹 애플리케이션에서 HTTP 요청을 처리하기 위한 자바 인터페이스를 정의하는 방법으로 이 인터페이스는 주로 스프링 프레임워크와 함께 사용되며, HTTP 요청을 보다 쉽게 관리하고 구현할 수 있도록 도와준다.

어노테이션을 사용하여 HTTP 메소드(GET, POST 등)와 URL 경로를 매핑할 수 있다.
사용 방법: HTTP 인터페이스는 두 단계로 사용되는데 첫 번째 단계는 어노테이션으로 선언된 메소드를 가진 인터페이스를 정의하는 것이고, 두 번째 단계는 이 인터페이스를 구현하는 프록시 객체를 생성하는 것이다.

2. 주요 특징

  • 프록시 객체 생성 : HTTPInterface를 통해 생성된 프록시 객체는 HTTP 요청을 쉽게 보낼 수 있는 기능을 제공한다.
  • WebClient 및 RestClient 기반: HTTPInterface는 Spring의 WebClient와 RestClient를 기반으로 하여, 비동기 및 동기 요청을 모두 지원한다.

1. Request.class

@NoArgsConstructor
@Getter
public class GeminiRequest {

    private List<Content> contents;     // 객체의 요청의 내용 담을 공간
    
    public GeminiRequest(String text) {
        Part part = new TextPart(text);
        Content content = new Content(Collections.singletonList(part));
        this.contents = Arrays.asList(content);
    }


    @Getter
    @AllArgsConstructor
    private static class Content{
        private List<Part> parts;
    }

    interface Part{}

    @Getter
    @AllArgsConstructor
    private static class TextPart implements Part{
        public String text;
    }
}
  • Gemini에서 요청하는 구조에 맞게 Dto를 구성해 준다.

2. Response.class

@NoArgsConstructor
@Getter
public class GeminiResponse {

    private List<Candidate> candidates;

    @Getter
    public static class Candidate{
        private Content content;
        private String finishReason;
        private int index;
        List<SafetyRating> safetyRatings;
    }

    @Getter
    public static class Content{
        private List<TextPart> parts;
        private String role;
    }

    @Getter
    public static class TextPart{
        private String text;
    }

    @Getter
    public static class SafetyRating{
        private String category;
        private String probability;
    }
}
  • 반환값도 Gemini에서 반환해주는 구조에 맞게 Dto를 구성해준다.

3. HTTPInteface

api 주소 : https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=YOUR_API_KEY

이처럼 api주소에 맞는 형식으로 구성해줘야 한다.

(1) GeminiInteface.class

@HttpExchange("/v1beta/models")
public interface GeminiInterface {

    @PostExchange("{model}:generateContent")
    GeminiResponse getCompletion(
            @PathVariable String model,
            @RequestBody GeminiRequest request
    );
}
  • @HttpExchange("/v1beta/models"):
    이 어노테이션은 GeminiInterface가 /v1beta/models 경로에 대한 HTTP 요청을 처리하는 인터페이스임을 나타냅니다. 즉, 이 경로로 들어오는 요청을 이 인터페이스의 메서드가 처리하게 됩니다.

  • @PostExchange("{model}:generateContent"):
    이 어노테이션은 HTTP POST 요청을 처리하는 메서드를 정의합니다. {model}은 경로 변수로, 실제 요청 시에 특정 모델 이름으로 대체됩니다. 예를 들어, /v1beta/models/someModel:generateContent와 같은 요청이 될 수 있습니다.

  • model부분은 AI API 버전이 바뀔때마다 유동적으로 하기 위해 외부에서 값을 가져오도록 합니다.


(2) Config.class

@Configuration
public class AppConfig {

    @Bean
    public RestClient geminiRestClient(@Value("${gemini.baseurl}") String baseUrl,
                                       @Value("${googleai.api.key}") String apikey) {
        return RestClient.builder()
                .baseUrl(baseUrl)
                .defaultHeader("x-goog-api-key",apikey)
                .defaultHeader("Content-Type", "application/json")
                .defaultHeader("Accept","application/json")
                .build();
    }


    @Bean
    public GeminiInterface geminiInterface(@Qualifier("geminiRestClient") RestClient restClient) {
        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
        return factory.createClient(GeminiInterface.class);
    }
}
  • gemini.baseurl과 googleai.api.key는 propertiy의 값을 주입받도록 구성합니다.

  • RestClient.builder()를 사용하여 RestClient를 구성하고, 기본 URL과 HTTP 헤더를 설정합니다.
    baseUrl: API의 기본 URL.
    x-goog-api-key: Google API 키.
    Content-Type 및 Accept: 요청과 응답의 데이터 형식을 JSON으로 설정합니다.

  • @Qualifier("geminiRestClient") RestClient restClient는 이전에 정의한 geminiRestClient 빈을 주입받습니다. @Qualifier는 여러 빈이 있을 때 특정 빈을 선택하는 데 사용됩니다.

  • RestClientAdapter.create(restClient)를 호출하여 RestClient를 RestClientAdapter로 변환합니다. 이는 REST 클라이언트를 HTTP 서비스 프록시로 변환하는 과정입니다.

  • HttpServiceProxyFactory.builderFor(adapter).build()를 사용하여 HTTP 서비스 프록시 팩토리를 생성합니다.

  • factory.createClient(GeminiInterface.class)를 호출하여 GeminiInterface의 구현체를 생성하고 반환합니다.


4. Service

@Service
@RequiredArgsConstructor
public class AiService {
    public static final String GEMINI_PRO = "gemini-pro";

    private final GeminiInterface geminiInterface;
    private final AiRepository aiRepository;


    public GeminiResponse getCompletion(GeminiRequest request){
        return geminiInterface.getCompletion(GEMINI_PRO, request);
    }

    public String getAiResponse(String message){
        GeminiRequest geminiRequest = new GeminiRequest(message+", 답변을 최대한 간결하게 50자 이하로 해줘");
        GeminiResponse response = getCompletion(geminiRequest);

        String aiResult = response.getCandidates()
                .stream()
                .findFirst().flatMap(candidate -> candidate.getContent().getParts()
                        .stream()
                        .findFirst()
                        .map(GeminiResponse.TextPart::getText))
                .orElse(null);
        return aiResult;
    }
}
  • 현재 Gemini의 버전을 final을 통해 명시해준다.
  • 유저에게 받은 Request를 getCompletion을 통해 이전에 만든 HTTPInteface의 HTTP 통신을 통해 Google Gemini에 해당 값을 보내고 이를 통해 받은 값을 Response에 맞춰 가져온다.

4. 결과

5. 문제상황

Google Gemini API JSON 통신관련 이슈

profile
Web Developer

0개의 댓글