spring / / 2024. 3. 6. 09:57

gemini java api with spring boot 사용해보기

스프링부트를 사용하여 gemini api 사용하는 방법을 알아보자.

gemini api를 google-cloud-vertexai로 이용하는 방법도 있지만, 여기서는 REST API를 통해 직접 사용해보기로 한다.

gemini rest api 사용하는 방법은 생각보다 어렵지 않다. chatgpt api 등 다른 외부 api 사용하는 것과 동일하며 특정 api에 요청을 보내고 응답을 받으면 된다. 물론 api를 사용하기 전에 필요한 api key를 발급을 받아야 한다.

이런 과정을 차례대로 아주 단순한 방법으로 한번 해보도록 하자.

인증 키 발급

우선 gemini api를 사용하기 위해서는 인증키가 있어야 한다.
아래 URL로 접속해서 인증키를 발급받자. (로그인 필요)

여기서 발급 받은 인증키를 복사해두자.

프로젝트 구성

java로 api를 사용하기 위한 프로젝트 구성은 아래와 같이 진행한다.

  • http tool: intellij http
  • Java version : 17
  • Spring boot version: 3.2.3

API 사전 테스트

우선 위에서 발급받은 인증키로 api가 잘 동작하는지 확인하자.
여기서는 intellij의 http를 사용하였는데, 다른 툴을 사용해도 무관하다. (Postman 등)

gemini에 질의를 하려면 아래 API를 사용해서 응답을 받을 수 있다.
https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=\

API에 대한 자세한 설명은 아래 링크를 참고하자.

https://ai.google.dev/tutorials/rest_quickstart


Text 입력

generateContent 메소드를 사용하여 입력 메시지에 대한 응답을 받을 수 있다. 만일 텍스트만 입력을 하는 경우는 gemini-pro 모델을 사용한다.

아래에서 <인증키> 부분을 위에서 발급받은 api key로 변경하고 실행해보자.

POST https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent
content-Type: application/json
x-goog-api-key: <인증키>

{
  "contents": [{
    "parts":[{
      "text": "서울 맛집을 추천해줘"
    }]
  }]
}

위의 코드를 실행하면 다음과 같은 결과가 나타나면 정상적으로 실행이 된 것이다.

응답결과
{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "text": "**전통 한국 요리**\n\n* **통인시장** - 전통 시장에서 다양한 가로수 먹거리, 떡볶이, 꼬치 등을 맛볼 수 있음.\n* **광장시장** - 닭갈비, 소고기 꼬치, 잡채 등의 전통 요리를 제공하는 길거리 음식으로 유명함.\n* **마포 구루국수** - 면과 김치, 고춧가루 등으로 만든 매콤한 수프 국수로 유명함.\n* **명동 교자** - 여러 가지 채우물로 만든 맛있는 교자로 유명함.\n* **동대문 떡볶이거리** - 다양한 떡볶이 가게가 즐비한 곳으로, 쫄깃한 면과 매콤한 소스를 맛볼 수 있음.\n\n**현대식 한국 요리**\n\n* **차메치나** - 현대적인 한식을 제공하는 유명 레스토랑으로, 미슐랭 스타를 획득함.\n* **오리엔탈 델리** - 창의적인 한식 요리를 제공하는 세련된 레스토랑으로, 현대적이고 전통적인 요리를 혼합함.\n* **봉구어탑** - 전통적인 한식을 모던한 방식으로 재해석한 퓨전 요리를 제공하는 레스토랑.\n* **산그림** - 미슐랭 스타를 획득한 레스토랑으로, 전통적인 한식 재료를 사용한 현대적인 요리를 제공함.\n* **아겟** - 한식과 서양 요리를 융합한 독특한 요리를 제공하는 레스토랑.\n\n**국제 요리**\n\n* **만원** - 정통 이탈리아 피자와 파스타를 제공하는 인기 레스토랑.\n* **빈앤빈** - 프랑스 크레페리로, 달콤하고 짭짤한 크레페를 다양하게 제공함.\n* **타코나미** - 양질의 멕시코 요리를 제공하는 타코 전문점으로, 다양한 토핑과 소스를 제공함.\n* **하라주쿠 파페** - 일본 크레이프와 빙수로 유명한 인기 디저트 카페.\n* **코코 ICHIBANYA** - 일본식 카레를 제공하는 체인점 레스토랑으로, 다양한 토핑과 매운맛 수준을 제공함."
          }
        ],
        "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"
      }
    ]
  }
}

API 개발

이제 java로 API를 개발해보자. 아래와 같이 개발에 필요한 의존성을 추가하자. 스프링 부트를 사용하여 gemini api를 사용해볼 것이다.

pom.xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.3</version>
    <relativePath/> 
</parent>

...

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

예제에서 Text 입력과 Text + Image 입력 두 가지를 만들어볼 것이다. 여기서는 gemini rest api 호출을 위해 스프링 6부터 지원하는 HttpInterface를 사용해 볼 것이다. HttpInterface는 HTTP 요청을 위한 서비스를 자바 인터페이스와 어노테이션만으로 사용할 수 있도록 하는 것이다. 만일 HttpInterface를 사용하지 않으려면 WebClient나 다른 클라이언트를 사용해도 된다.

데이터 구조 정의

REST API 호출 시 사용되는 데이터의 입력과 출력 클래스를 정의해보자. 여기서는 class를 사용하지만 record를 사용해도 무방하다.

[GeminiRequest]

@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);
    }

    public GeminiRequest(String text, InlineData inlineData) {
        List<Content> contents = List.of(
            new Content(
                List.of(
                    new TextPart(text),
                    new InlineDataPart(inlineData)
                )
            )
        );

        this.contents = contents;
    }

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

    interface Part {}

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

    @Getter
    @AllArgsConstructor
    private static class InlineDataPart implements Part {
        public InlineData inlineData;
    }

    @Getter
    @AllArgsConstructor
    public static class InlineData {
        private String mimeType;
        private String data;
    }
}

데이터 입력 구조에 맞게 정의한다.

[출력 클래스]

@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;
    }
}

데이터 출력 구조에 맞게 정의했다.

HttpInterface 정의

Gemini rest api를 호출해주는 HttpInterface를 정의하자.

api주소는 아래와 같다.
<base-url>/v1beta/models/<model>:generateContent

예: https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent

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

    @PostExchange("{model}:generateContent")
    GeminiResponse getCompletion(
        @PathVariable String model,
        @RequestBody GeminiRequest request
    );
}

그리고 RestClient에서 사용하는 header 값을 정의해주자.

@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 client) {
        RestClientAdapter adapter = RestClientAdapter.create(client);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
        return factory.createClient(GeminiInterface.class);
    }
}

위에서 gemini.baseurl과 googleai.api.key는 application.yml에 정의해서 사용할 것이다.

[application.yml]

gemini.baseurl: https://generativelanguage.googleapis.com
googleai.api.key: <인증키>

여기에 인증키는 위에서 발급받은 gemini api key를 입력해준다.

스프링부트 생성

스프링 부트 애플리케이션을 생성한다.

@SpringBootApplication
public class GeminiSimpleApplication {

    public static void main(String[] args) {
        SpringApplication.run(GeminiSimpleApplication.class, args);
    }
}

이제 서비스 호출을 위한 기반 준비는 다 됬고 이제 Text, image를 전송하는 서비스 클래스를 만들어실행해보자.

1. Text 입력

@Service
public class GeminiService {
    public static final String GEMINI_PRO = "gemini-pro";
    public static final String GEMINI_ULTIMATE = "gemini-ultimate";
    public static final String GEMINI_PRO_VISION = "gemini-pro-vision";

    private final GeminiInterface geminiInterface;

    @Autowired
    public GeminiService(GeminiInterface geminiInterface) {
        this.geminiInterface = geminiInterface;
    }

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

    public String getCompletion(String text) {
        GeminiRequest geminiRequest = new GeminiRequest(text);
        GeminiResponse response = getCompletion(geminiRequest);

        return response.getCandidates()
            .stream()
            .findFirst().flatMap(candidate -> candidate.getContent().getParts()
                .stream()
                .findFirst()
                .map(GeminiResponse.TextPart::getText))
            .orElse(null);
    }
}

getCompletion에 text로 구성된 GeminiRequest 객체를 넘겨주면 geminiInterface에서 HttpInterface를 통해 gemini rest api를 호출하고 응답을 리턴한다.

실제로 잘 동작하는지 테스트 케이스를 통해 실행해보자.

@SpringBootTest
class GeminiServiceTest {
    @Autowired
    private GeminiService service;

    @Test
    void getCompletion() {
        String text = service.getCompletion("서울 맛집을 추천해줘");
        System.out.println(text);
    }
}

실행한 결과는 아래와 같다.

**전통 한식**

* **연두옥** (종로구): 1992년부터 운영 중인 전통 가마솥밥집으로, 부드럽고 풍미 있는 가마솥밥으로 유명합니다.
* **사랑방** (강남구): 전통 한옥에서 다양한 한식 요리를 제공하며, 특히 갈비찜과 순두부찌개가 인기 있습니다.
* **평창골 목걸이** (강남구): 북한 음식을 전문으로 하는 레스토랑으로, 평양냉면, 옥수수와 고기로 만든 풍년밥, 신선한 해물 요리가 유명합니다.

**현대적 한식**

* **명동 교자** (중구): 1953년부터 영업하는 국수집으로, 쫄깃한 국수와 다양한 면 종류로 유명합니다.
* **오신채방** (종로구): 채소가 풍부한 현대적 한식 요리를 제공하며, 특히 밥과 면류가 인기 있습니다.
* **드럼스틱** (강남구): 프라이드 치킨과 맥주를 전문으로 하는 곳으로, 다양한 맛과 크기의 튀김닭을 제공합니다.

**인터내셔널**

* **슈크레트** (강남구): 터키 음식을 전문으로 하는 레스토랑으로, 케밥, 막내, 바클라바 등의 정통 터키 요리를 제공합니다.
* **모리스 바게터리** (강남구): 프랑스 스타일의 바게트와 페이스트리를 제공하는 베이커리로, 신선한 빵과 다양한 페이스트리로 유명합니다.
* **벨지안 프라이스** (강남구): 벨기에 감자튀김을 전문으로 하는 곳으로, 다양한 따뜻한 소스와 함께 제공합니다.

**카페와 디저트**

* **스타벅스 리저브 메가 플래그십 스토어** (강남구): 세계에서 가장 큰 스타벅스 매장으로, 다양한 커피와 차, 구운 제품을 제공합니다.
* **카페 베네** (종로구): 여러 지점을 두고 있는 커피 프랜차이즈로, 다양한 커피와 티 메뉴, 간식을 제공합니다.
* **바스킨라빈스** (강남구): 다양한 아이스크림 맛과 셰이크를 제공하는 국제적인 아이스크림 프랜차이즈입니다.

**가성비**

* **서울 국민식당** (종로구): 저렴한 가격으로 다양한 한국 요리를 제공하는 인기 있는 식당입니다.
* **종로 순두부마을** (종로구): 다양한 순두부 요리를 제공하는 가성비 좋은 식당입니다.
* **이화마을 쫄면** (종로구): 쫄깃한 쫄면과 신선한 재료로 유명한 인기 있는 가게입니다.

정상적으로 잘 응답되는 것을 확인할 수 있다.

2. Text + Image 입력

이번에는 하나의 이미지에 대해 설명을 해달라는 요청을 해볼 것이다.

이 이미지를 업로드하면서 "이 사진에 대해 설명해줘"라고 gemini에게 물어볼 것이다. 입력값에 text와 image가 있는 경우는 gemini-pro-vision 모델을 사용한다.

@Service
public class GeminiService {
    public static final String GEMINI_PRO = "gemini-pro";
    public static final String GEMINI_ULTIMATE = "gemini-ultimate";
    public static final String GEMINI_PRO_VISION = "gemini-pro-vision";

    ...

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

    public String getCompletionWithImage(String text, GeminiRequest.InlineData inlineData) {
        GeminiRequest geminiRequest = new GeminiRequest(text, inlineData);
        GeminiResponse response = getCompletionWithImage(geminiRequest);

        return response.getCandidates()
            .stream()
            .findFirst().flatMap(candidate -> candidate.getContent().getParts()
                .stream()
                .findFirst()
                .map(GeminiResponse.TextPart::getText))
            .orElse(null);
    }
}

이미지를 resource 하위에 복사한다. (boy.jpg)

그리고 테스트 케이스를 통해 실행해보자.

@SpringBootTest
class GeminiServiceTest {
    @Autowired
    private GeminiService service;

    @Test
    void describeAnImage() throws Exception {
        GeminiRequest.InlineData inlineData = new GeminiRequest.InlineData(
            "image/jpeg",
            Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of("src/main/resources/", "boy.jpg")))
        );

        String text = service.getCompletionWithImage(
                "이 이미지를 설명해줘",
                inlineData
        );
        System.out.println(text);
    }
}

실행한 결과는 아래와 같다.

한 소년이 숟가락으로 우유에 떠 있는 시리얼을 먹고 있습니다. 그는 옆을 보고 있고, 오렌지 주스 한 잔이 그 옆에 있습니다.

위와 같이 별로 어렵지 않게 gemini api 를 사용할 수 있는 것 같다. 다음에 시간되면 Vertex AI Gemini API로 한번 사용해봐야겠다.

https://cloud.google.com/vertex-ai/docs/generative-ai/start/quickstarts/quickstart-multimodal?hl=ko

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유