이번에 프로젝트를 진행하다가 AI를 연동하여 기능을 구현해보면 어떨까라는 생각을 가지게 되어 Google에서 제공하는 Gemini AI를 연동해보기로 하였다.
Gemini API를 사용하기 위해서는 인증 키가 필요한다. 따라서 Google Gemini 사이트에 가서 발급받을 수 있다.

외부 API와 통신하기 위해서는 Request / Response의 구조를 파악해야 한다.
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'
{
"contents": [{
"parts":[{
"text": "서울 맛집을 추천해줘"
}]
}]
}
{
"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"
}
]
}
}
통신하기 위해서는 여러 방식중에 HttpInterface를 사용하도록 하겠다.
1. HTTPInterface란?
HTTP 인터페이스는 웹 애플리케이션에서 HTTP 요청을 처리하기 위한 자바 인터페이스를 정의하는 방법으로 이 인터페이스는 주로 스프링 프레임워크와 함께 사용되며, HTTP 요청을 보다 쉽게 관리하고 구현할 수 있도록 도와준다.
어노테이션을 사용하여 HTTP 메소드(GET, POST 등)와 URL 경로를 매핑할 수 있다.
사용 방법: HTTP 인터페이스는 두 단계로 사용되는데 첫 번째 단계는 어노테이션으로 선언된 메소드를 가진 인터페이스를 정의하는 것이고, 두 번째 단계는 이 인터페이스를 구현하는 프록시 객체를 생성하는 것이다.
2. 주요 특징
- 프록시 객체 생성 : HTTPInterface를 통해 생성된 프록시 객체는 HTTP 요청을 쉽게 보낼 수 있는 기능을 제공한다.
- WebClient 및 RestClient 기반: HTTPInterface는 Spring의 WebClient와 RestClient를 기반으로 하여, 비동기 및 동기 요청을 모두 지원한다.
@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;
}
}
@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;
}
}
이처럼 api주소에 맞는 형식으로 구성해줘야 한다.
@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 버전이 바뀔때마다 유동적으로 하기 위해 외부에서 값을 가져오도록 합니다.
@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의 구현체를 생성하고 반환합니다.
@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;
}
}
