๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿš€ ํŒ (๊ธฐ์ˆ  ์ ์šฉ ๋ฐฉ๋ฒ• ๋“ฑ)/๐Ÿค– Spring AI

[Spring AI ๐Ÿค–] ์ด๋ฏธ์ง€๋ฅผ ๋ถ„์„ํ•˜๊ณ  JSON ํฌ๋งทํŒ…์„ ํ•ด๋ณด์ž! (feat. EatToFit)

by dev_writer 2024. 9. 29.

์•ˆ๋…•ํ•˜์„ธ์š” dev_writer์ž…๋‹ˆ๋‹ค.

 

์š”์ฆ˜ ํšŒ์‚ฌ ์ผ๊ณผ ๋‹ค๋ฅธ ์ผ๋“ค๋กœ ์ธํ•ด ๋ธ”๋กœ๊ทธ ์ž‘์„ฑ์„ ์ž ์‹œ ํ•˜์ง€ ์•Š์•˜์—ˆ๋Š”๋ฐ์š”, ์ด๋ฒˆ์— ๋‹ค์‹œ EatToFit ํ”„๋กœ์ ํŠธ๋ฅผ ํ•˜๋ฉด์„œ Spring AI๋ฅผ ์—ฐ๊ฒฐํ•œ ๊ณผ์ •์ด ์žˆ์–ด ์ด ๋ถ€๋ถ„์„ ๊ณต์œ ๋“œ๋ฆฌ๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

 

๊ฐœ๋ฐœ ๊ณ„๊ธฐ: ์Œ์‹ AI API ๋งŒ๋ฃŒ ๋ฌธ์ œ ๋ฐœ์ƒ ๐Ÿ™…

์‚ฌ์‹ค Spring AI๋ฅผ ์ ์šฉํ•˜๋Š” ๊ฒƒ์€ ์Œ์‹ ๊ฒ€์ƒ‰์ด ์•„๋‹ˆ๋ผ ์Œ์‹๊ณผ ํšŒ์›์˜ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์šด๋™ ํ”Œ๋žœ์„ ์ƒ์„ฑํ•  ๋•Œ์—๋งŒ ์ง„ํ–‰ํ•  ๊ณ„ํš์ด์—ˆ์Šต๋‹ˆ๋‹ค.

 

๊ธฐ์กด์—๋Š” ์Œ์‹ AI ํšŒ์‚ฌ๋กœ๋ถ€ํ„ฐ API๋ฅผ ๋ฐœ๊ธ‰๋ฐ›์•„์„œ, ๋ถ„์„ ํ’ˆ์งˆ๊ณผ ์†๋„, ์‹ ๋ขฐ์„ฑ ๋ฉด์—์„œ ๋ชจ๋‘ ์šฐ์ˆ˜ํ•œ ์Œ์‹ AI ํšŒ์‚ฌ์˜ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ๊ณ„ํš์ด์—ˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋Ÿฌ๋‚˜ API๋ฅผ ๋ฐ›์•˜๋˜ ์‹œ์ ์ด 5์›”์ด์—ˆ๋Š”๋ฐ, ๊ทธ๋ž˜์„œ์ธ์ง€ ์žฌ์‹œ๋„ํ•œ ๊ฒฐ๊ณผ ๋งŒ๋ฃŒ๋˜์—ˆ๋‹ค๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ๋˜์–ด ๋‹น์žฅ์€ ํ”„๋กœ์ ํŠธ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (์Œ์‹ AI ํšŒ์‚ฌ์— ๋ฌธ์˜๋ฅผ ๋“œ๋ฆฌ๊ธด ํ–ˆ์ง€๋งŒ ๋‹ต๋ณ€์ด ์˜ค๋Š” ๋ฐ์—๋„ ์ถ”๊ฐ€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ๊ฒƒ์œผ๋กœ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.)

 

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด, ์šฐ์„ ์€ ์Œ์‹์„ ๊ฒ€์ƒ‰ํ•˜๋Š” ๊ธฐ๋Šฅ์—๋„ Spring AI๋ฅผ ์ด์šฉํ•ด ๋ณด์ž๋Š” ๊ฒฐ๋ก ์„ ๋‚ด๋ฆฌ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

Spring AI ์ ์šฉํ•˜๊ธฐ (1.0.0 M2 ๋ฒ„์ „)

๋จผ์ €, Spring AI๋Š” 24๋…„ 9์›” ๊ธฐ์ค€์œผ๋กœ M2 ๋ฒ„์ „์ด ๋‚˜์™”์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ž˜์„œ์ธ์ง€ build.gradle ์ž‘์„ฑ ์ฝ”๋“œ๊ฐ€ ์กฐ๊ธˆ ๋‹ฌ๋ผ์กŒ๋Š”๋ฐ, ์ž์นซํ•˜๋ฉด ๊ธฐ์กด์— ์‚ฌ์šฉ ์ค‘์ด๋˜ lombok ๋ฐ ๋‹ค๋ฅธ ์Šคํ”„๋ง ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ์ถฉ๋Œํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์‚ฌ์‹ค์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค.

 

์Šคํ”„๋ง๋ถ€ํŠธ ์Šคํƒ€ํ„ฐ ์‚ฌ์ดํŠธ์—์„œ ์ฒ˜์Œ๋ถ€ํ„ฐ Spring AI ๋ฐ ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ (lombok ๋“ฑ)๋ฅผ ํ•จ๊ป˜ ์ ์šฉํ•˜์‹ค ๋ถ„๋“ค์€ ๊ดœ์ฐฎ์œผ์‹œ๊ฒ ์ง€๋งŒ, ์ €์ฒ˜๋Ÿผ ์ดํ›„์— Spring AI๋ฅผ ์ ์šฉํ•˜๋ ค๋Š” ๋ถ„๋“ค์„ ์œ„ํ•ด build.gradle ์ฝ”๋“œ๋ฅผ ๊ณต์œ ํ•ด ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

 

build.gradle

repositories {
    mavenCentral()
    // ์ถ”๊ฐ€
    maven { url 'https://repo.spring.io/milestone' }
}

ext {
    jwtVersion = '0.12.6'
    snippetsDir = file('build/generated-snippets')
    // ์ถ”๊ฐ€
    set('springAiVersion', "1.0.0-M2")
}

dependencies {
    ...
    // ์ถ”๊ฐ€
    implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
}

// ์ถ”๊ฐ€
dependencyManagement {
    imports {
        mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
    }
}

...

 

application.yml (OpenAI ๊ธฐ์ค€)

application.yml์ด๋“  application-local.yml ์ด๋“  ๊ด€๊ณ„์—†์ด spring.ai.openai.api-key์— OpenAI ํ‚ค๋ฅผ ์ž‘์„ฑํ•ด ๋‘์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

AiConfig.java

๋‹ค์Œ์œผ๋กœ๋Š” Spring AI์˜ ChatClient๋ฅผ ์ •์˜ํ•  AiConfig ์„ค์ • ํŒŒ์ผ์„ global/config์— ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

@Configuration
public class AiConfig {

    @Value("${spring.ai.openai.api-key}")
    private String apiKey;

    @Bean
    public ChatClient chatClient() {
        ChatModel chatModel = new OpenAiChatModel(new OpenAiApi(apiKey));
        return ChatClient
                .builder(chatModel)
                .build();
    }
}

 

ChatClient๋Š” ์‹ค์ œ์ ์œผ๋กœ Spring AI๋กœ๋ถ€ํ„ฐ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ด๋ฉฐ, ChatModel์€ ์‚ฌ์šฉํ•  AI ๊ตฌํ˜„์ฒด๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ด์ „์— ์ž‘์„ฑํ•œ ๊ธ€์„ ์ฐธ๊ณ ํ•˜์‹œ๋ฉด ChatClient์™€ ChatModel์— ๋Œ€ํ•ด ์ตํžˆ์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

JSON ํฌ๋งทํŒ…์€ ์–ด๋–ป๊ฒŒ ํ• ๊นŒ?

LLM์—๊ฒŒ ํŠน์ • JSON ํฌ๋งท์œผ๋กœ ๋ฐ˜ํ™˜ํ•ด ๋‹ฌ๋ผ๋Š” ๊ฒƒ์€ ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ๋นˆ๋ฒˆํžˆ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค.

 

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด, ๊ธฐ์กด์—๋Š” ํ”„๋กฌํ”„ํŠธ ๋ง๋ฏธ์— ์–ด๋–ค JSON ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ํ•ด ๋‹ฌ๋ผ๊ณ  ์ง์ ‘ ์ •์˜๋ฅผ ํ•ด์ค˜์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๋งŒ์•ฝ ์ผ์ข…์˜ JSON ๋ณ€ํ™˜๊ธฐ๊ฐ€ ์žˆ๋‹ค๋ฉด, ์ด ๋ถ€๋‹ด์„ ์‰ฝ๊ฒŒ ๋œ์–ด๋‚ผ ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ์š”?

 

์ด๋ฅผ ์œ„ํ•ด Spring AI์—์„œ๋Š” BeanOutputConverter๋ฅผ ์ •์˜ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

1. BeanOutputConverter ์ƒ์„ฑ

Spring AI์˜ BeanOutputConverter ํด๋ž˜์Šค

 

๋งˆ์ง€๋ง‰์— ์žˆ๋Š” BeanOutputConverter(Class<T> clazz)๋ฅผ ํ†ตํ•ด, ํŠน์ • ํด๋ž˜์Šค์— ๋งž๋Š” ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ๋” ๋ณด์‹œ๋ฉด, ์ปค์Šคํ…€ objectMapper๋„ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.)

 

2. getFormat ํ•จ์ˆ˜ ํ˜ธ์ถœ

getFormat ํ•จ์ˆ˜

 

getFormat ํ•จ์ˆ˜์—์„œ๋Š” LLM์—๊ฒŒ ์ „๋‹ฌํ•  ๋ช…๋ น์–ด๋ฅผ ํฌํ•จํ•˜์—ฌ ์ผ์ข…์˜ ํ”„๋กฌํ”„ํŠธ๋กœ ๋งŒ๋“ค์–ด๋ƒ…๋‹ˆ๋‹ค.

 

์ฆ‰, ์š”๊ตฌํ•˜๋Š” json ์Šคํ‚ค๋งˆ์— ๋งž์ถ”์–ด ์‘๋‹ตํ•˜๋ผ๊ณ  ๋ช…๋ นํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. (์ €ํฌ๊ฐ€ ์ง์ ‘ JSON์œผ๋กœ ์‘๋‹ตํ•ด์•ผ ํ•œ๋‹ค๊ณ  ์ž‘์„ฑํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.)

 

3. convert ํ•จ์ˆ˜ ํ˜ธ์ถœ

convert ํ•จ์ˆ˜

 

์ดํ›„, convert ํ•จ์ˆ˜์—์„œ ์ฃผ์–ด์ง„ text๋ฅผ objectMapper๋ฅผ ์ด์šฉํ•˜์—ฌ ํด๋ž˜์Šค๋กœ ๋งŒ๋“ค์–ด๋ƒ…๋‹ˆ๋‹ค. ๋ฐ˜ํ™˜ ๋„์ค‘ ์˜ˆ์™ธ (JsonProcessingException)๊ฐ€ ๋ฐœ์ƒํ•  ์‹œ ์ผ๋ฐ˜์ ์ธ RuntimeException์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

 

์ •๋ฆฌ

๋”ฐ๋ผ์„œ BeanOutputConverter๋ฅผ ์ด์šฉํ•˜๋Š” ์ˆœ์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. BeanOutputConverter๋ฅผ ์š”๊ตฌํ•˜๋Š” ํด๋ž˜์Šค์— ๋งž๊ฒŒ ์„ ์–ธ
  2. getFormat ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ”„๋กฌํ”„ํŠธ ๊บผ๋‚ด๊ธฐ, LLM์— ์ „๋‹ฌ
  3. ๋ฐ˜ํ™˜๋œ LLM ์‘๋‹ต์„ convert ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์š”๊ตฌํ•˜๋Š” ํด๋ž˜์Šค๋กœ ๋ณ€ํ™˜

 

์ด๋ฏธ์ง€ ๋ถ„์„ํ•˜๊ธฐ

์›๋ฆฌ

๋‹ค์Œ์œผ๋กœ๋Š” ์ด๋ฏธ์ง€ ๋ถ„์„์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์ฒ˜์Œ์—๋Š” ์ด๋ฏธ์ง€์™€ ๊ด€๋ จ๋˜๊ธฐ ๋•Œ๋ฌธ์— chat ๊ด€๋ จ ๋ถ€๋ถ„์ด ์•„๋‹ˆ๋ผ image ๊ด€๋ จ ๋ถ€๋ถ„์„ ์•Œ์•„๋ด์•ผ ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์ƒ๊ฐํ–ˆ์—ˆ๋Š”๋ฐ, Spring AI์˜ image๋Š” ์ด๋ฏธ์ง€๋ฅผ ๋งŒ๋“ค์–ด๋‚ด๋Š” ๋ฐ ์ง‘์ค‘๋˜์–ด ์žˆ์œผ๋ฉฐ, ์ด๋ฏธ์ง€๋ฅผ ๋ถ„์„ํ•˜๋Š” ๊ฒƒ์€ chat๊ณผ ๊ด€๋ จ๋˜์–ด ์žˆ๋‹ค๋Š” ์‚ฌ์‹ค์„ ๋ฐœ๊ฒฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

์ €ํฌ๋Š” ์ด๋ฏธ์ง€์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ URL๋กœ์จ ์ œ๊ณตํ•  ๊ฒƒ์œผ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ChatClient์—๋Š” ๋‘ ๊ฐ€์ง€์˜ prompt ๋ฐฉ์‹์ด ์žˆ๋Š”๋ฐ, Promt ์ธ์ž๋ฅผ ๋ฐ›๋Š” ๋ฐฉ์‹๊ณผ ๋ฐ›์ง€ ์•Š๋Š” ๋ฐฉ์‹์ด ์žˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  Prompt ์ธ์ž๋ฅผ ๋ฐ›์ง€ ์•Š๋Š” ๋ฐฉ์‹์€ ChatClientRequestSpec์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ, Prompt ์ธ์ž๋ฅผ ๋ฐ›๋Š” ๋ฐฉ์‹์€ ChatClientPromptRequestSpec์„ ๋ฐ›์Šต๋‹ˆ๋‹ค.

 

Spring AI์˜ ChatClient (ํ•˜๋‹จ์— ๋‘ prompt ํ•จ์ˆ˜ ์œ„์น˜)

 

์˜ˆ์ „์— Spring AI ๊ธ€์„ ์ž‘์„ฑํ–ˆ์„ ๋•Œ ๋ณด์‹  ๋ถ„๋“ค์€ ์•Œ๊ฒ ์ง€๋งŒ, Prompt๋Š” ์ผ๋ฐ˜ ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

 

๊ทธ๋ ‡๊ธฐ์— ๋งŒ์•ฝ ํŒŒ์ผ์„ ์˜ฌ๋ฆฌ๋Š” ๋“ฑ ๋ถ€๊ฐ€์ ์ธ ์ •๋ณด๋ฅผ ๋„˜๊ธฐ๊ณ ์ž ํ•œ๋‹ค๋ฉด, Prompt๋ฅผ ๋ฐ›์ง€ ์•Š๋Š” prompt ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

์ด ๋ง์€ ๊ณง PromptTemplate์˜ ์ด์ ์„ ๋ˆ„๋ฆด ์ˆ˜ ์—†๋‹ค๋Š” ๋œป์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ๋ฌผ๋ก  ํ”„๋กฌํ”„ํŠธ (ํ…์ŠคํŠธ)๋ฅผ ๋งŒ๋“œ๋Š” ๊ณผ์ •์—์„œ ๋ถ€๋ถ„์ ์œผ๋กœ PromptTemplate์„ ์“ธ ์ˆ˜๋„ ์žˆ๊ฒ ์ง€๋งŒ, prompt ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•  ๋•Œ์—๋Š” ์ฒซ ๋ฒˆ์งธ์˜ prompt ํ•จ์ˆ˜๋ฅผ ์จ์•ผ ํ•ฉ๋‹ˆ๋‹ค. (ํŒŒ์ผ ๋“ฑ ๋ถ€๊ฐ€ ์ •๋ณด๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด)

 

Prompt๋ฅผ ๋ฐ›๋Š” ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜์ธ ChatClientPromptRequestSpec์€ ๋ณ„๋„์˜ ์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์—†์œผ๋ฉฐ, ๋ฐ”๋กœ call์„ ํ•˜์—ฌ ์‘๋‹ต์„ ๋ฐ›๋Š” ๊ฒƒ๋งŒ ๋‚˜์˜ต๋‹ˆ๋‹ค.

ChatClientPromptRequestSpec

 

๋ฐ˜๋ฉด Prompt๋ฅผ ๋ฐ›์ง€ ์•Š๋Š” ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜์ธ ChatClientRequestSpec์—์„œ๋Š” ์—ฐ์‡„์ ์œผ๋กœ ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒŒ ๋ณด์ด๋ฉฐ, ์ด ์ค‘ Consumer<PromptUserSpec> consumer๋ฅผ ๋ฐ›๋Š” ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•˜๋ฉด ํŒŒ์ผ ์ •๋ณด๋ฅผ ๋ถ™์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

ChatClientRequestSpec

 

์ด๋Š” PromptUserSpec์— media๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

PromptUserSpec

 

์‹ค์Šต (JSON ์ปจ๋ฒ„ํ„ฐ ํฌํ•จ)

๊ทธ๋Ÿฌ๋ฉด ์ด์ œ ์ตœ์ข… ์‹ค์Šต์„ ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

1. ์‘๋‹ต DTO ์ •์˜

๋จผ์ €, ํ”„๋กœ์ ํŠธ์— ํ•„์š”ํ•œ ์‘๋‹ต DTO๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

 

์ €๋Š” ์Œ์‹ ๋ถ„์„์— ๋งž๊ฒŒ FoodSearchResponse๋ฅผ ์ •์˜ํ•˜๊ณ , ์ด๋ฅผ ๋ฆฌ์ŠคํŠธ๋กœ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” PredictFoodSearchResponse๋ฅผ ์ถ”๊ฐ€๋กœ ์ •์˜ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

// eattofit > food > infrastructure > dto

public record PredictFoodSearchResponse(
        List<FoodSearchResponse> predictFoods
) {
}

public record FoodSearchResponse(
        String name,
        BigDecimal servingSize,
        String unit,
        BigDecimal kcal,
        BigDecimal carbohydrate,
        BigDecimal protein,
        BigDecimal fat,
        BigDecimal sodium
) {
}

 

2. FoodSearchManager ์ •์˜

๋‹ค์Œ์œผ๋กœ๋Š” ์‘๋‹ต์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ๊ธฐ๋Šฅํ•˜๋Š” ๊ตฌํ˜„์ฒด๋“ค์„ ์ถ”์ƒํ™”ํ•˜๊ธฐ ์œ„ํ•ด FoodSearchManager๋ฅผ ์ •์˜ํ•˜์˜€์Šต๋‹ˆ๋‹ค. (ํ–ฅํ›„ ์Œ์‹ AI ํšŒ์‚ฌ ๋“ฑ ๋‹ค๋ฅธ ๊ตฌํ˜„์ฒด๊ฐ€ ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋ผ ํŒ๋‹จ)

// eattofit > food > domain

public interface FoodSearchManager {

    PredictFoodSearchResponse search(String url);
}

 

3. FoodService ์ •์˜

FoodSearchManager๋ฅผ ์„œ๋น„์Šค์™€ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

// eattofit > food > application

@RequiredArgsConstructor
@Service
public class FoodService {

    private final FoodSearchManager foodSearchManager;

    public PredictFoodSearchResponse foodSearch(final String url) {
        return foodSearchManager.search(url);
    }
}

 

4. FoodController ์ •์˜

์„œ๋น„์Šค๋ฅผ ์ปจํŠธ๋กค๋Ÿฌ์™€ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

// eattofit > food > ui

@RequiredArgsConstructor
@RequestMapping("/api/foods")
@RestController
public class FoodController {

    private static final String IMAGE_URL = "image_url";

    private final FoodService foodService;

    @GetMapping("/search")
    public ResponseEntity<PredictFoodSearchResponse> search(final @RequestParam(value = IMAGE_URL) String imageUrl) {
        PredictFoodSearchResponse response = foodService.foodSearch(imageUrl);
        return ResponseEntity.ok()
                .body(response);
    }
}

 

5. OpenAiFoodSearchManager ์ •์˜ (ํ•ต์‹ฌ)

FoodSearchManager๋ฅผ ๊ตฌํ˜„ํ•œ OpenAiFoodSearchManager๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

 

AiConfig.java๋ฅผ ํ†ตํ•ด OpenAI ์ „์šฉ์˜ ChatClient๋ฅผ ๋ฐ›์•˜์œผ๋‹ˆ, ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

// eattofit > food > infrastructure

@RequiredArgsConstructor
@Component
public class OpenAiFoodSearchManager implements FoodSearchManager {

    private final ChatClient chatClient;

    @Override
    public PredictFoodSearchResponse search(final String url) {
        BeanOutputConverter<PredictFoodSearchResponse> parser = new BeanOutputConverter<>(PredictFoodSearchResponse.class);

        String response = chatClient.prompt()
                .user(userSpec -> {
                    try {
                        userSpec.text(
                                """
                                <role>
                                ๋„ˆ๋Š” 20๋…„ ์ด์ƒ ์Œ์‹ ๊ฒฝ๋ ฅ์„ ๊ฐ€์ง„ ์š”๋ฆฌ์‚ฌ ๊ฒธ ์˜์–‘์‚ฌ๋‹ค. ์Œ์‹ ์‚ฌ์ง„์„ ์ฃผ๋ฉด, ์ œ๊ณต๋œ format์— ๋งž์ถฐ ์‘๋‹ตํ•ด์•ผ ํ•œ๋‹ค.
                                </role>
                                <instruction>
                                1. ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ์Œ์‹์€ ์ตœ์†Œ 3๊ฐœ ์ด์ƒ ๋‚˜์™€์•ผ ํ•œ๋‹ค.
                                2. unit์€ 'g'๋กœ ๊ณ ์ •ํ•œ๋‹ค.
                                3. servingSize๋Š” gram ๊ธฐ์ค€์œผ๋กœ ๊ณ„์‚ฐํ•œ๋‹ค.
                                4. servingSize, kcal, carbohydrate, protein, fat, sodium์€ ๋ชจ๋‘ ์†Œ์ˆ˜์  ๋‘˜์งธ ์ž๋ฆฌ๊นŒ์ง€ ํ‘œํ˜„๋˜์–ด์•ผ ํ•œ๋‹ค.
                                5. ์†Œ์ˆ˜์  ๋‘˜์งธ ์ž๋ฆฌ๋“ค๋กœ ํ‘œํ˜„์„ ํ•  ๋•Œ, ์ด๊ฒƒ๋“ค์ด ๋ชจ๋‘ 0์œผ๋กœ ๋œ ๊ฒฝ์šฐ๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ๋„ ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค. (0์œผ๋กœ ๋œ ๊ฒฝ์šฐ๊ฐ€ ์žˆ์„ ์ˆ˜๋„ ์žˆ๋‹ค.)
                                6. name์€ ํ•œ๊ธ€ ๊ธฐ์ค€์œผ๋กœ ๋‚˜์™€์•ผ ํ•œ๋‹ค.
                                </instruction>
                                <example>
                                ์˜ˆ์‹œ ์‘๋‹ต์„ ์•Œ๋ ค์ฃผ๊ฒ ๋‹ค. ์˜ˆ์‹œ ์‘๋‹ต๊ณผ ๊ฐ™์€ ์Œ์‹์ด ๋‚˜์˜ค๋”๋ผ๋„ ์ด ์˜ˆ์‹œ ๊ฐ’์„ ๋˜‘๊ฐ™์ด ํ™œ์šฉํ•˜์ง€๋Š” ๋งˆ๋ผ.
                                {
                                    "predictFoods": [
                                        {
                                            "name": "๋น„๋น”๋ฐฅ",
                                            "servingSize": 488.02,
                                            "unit": "g",
                                            "kcal": 635.31,
                                            "carbohydrate": 97.13,
                                            "protein": 24.21,
                                            "fat": 16.23,
                                            "sodium": 1248.24
                                        },
                                        {
                                            "name": "์‚ฐ์ฑ„๋น„๋น”๋ฐฅ",
                                            "servingSize": 433.24,
                                            "unit": "g",
                                            "kcal": 596.83,
                                            "carbohydrate": 93.21,
                                            "protein": 17.82,
                                            "fat": 16.23,
                                            "sodium": 1135.35
                                        },
                                        {
                                            "name": "๋Œ์†ฅ๋น„๋น”๋ฐฅ",
                                            "servingSize": 430.24,
                                            "unit": "g",
                                            "kcal": 656.51,
                                            "carbohydrate": 93.23,
                                            "protein": 17.39,
                                            "fat": 18.66,
                                            "sodium": 1135.35
                                        }
                                    ]
                                }
                                </example>
                                format์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.
                                """ + parser.getFormat())
                                .media(MimeTypeUtils.IMAGE_JPEG, new UrlResource(url));
                    } catch (MalformedURLException e) {
                        throw new BadImageUrlException();
                    }
                })
                .call()
                .content();
        return parser.convert(response);
    }
}

 

  • ChatClient๋ฅผ ์ฃผ์ž…๋ฐ›์•˜์Šต๋‹ˆ๋‹ค.
  • PredictFoodSearchResponse์— ๋งž์ถ˜ BeanOutputConverter๋ฅผ ์ •์˜ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • chatClient.prompt()๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , user ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•จ๊ณผ ๋™์‹œ์— userSpec์„ ์ด์šฉํ•˜์—ฌ ๊ธฐ๋ณธ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ํ”„๋กฌํ”„ํŠธ ๋งˆ์ง€๋ง‰์—๋Š” BeanOutputConverter์˜ json ์Šคํ‚ค๋งˆ๋ฅผ ๋„ฃ์—ˆ์Šต๋‹ˆ๋‹ค.
    • media ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•˜์—ฌ ํŒŒ์ผ ์ •๋ณด๋ฅผ ๋„ฃ์—ˆ์Šต๋‹ˆ๋‹ค. ํŒŒ์ผ์˜ URL์„ ๋„ฃ๊ณ , ์ „๋‹ฌ์€ IMAGE_JPEG๋กœ ๊ณ ์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    • ๋‹น๊ทผ์—์„œ์˜ LLM ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž‘์„ฑํ•ด ๋ดค์Šต๋‹ˆ๋‹ค. ์—ญํ• ์„ ๋ถ€์—ฌํ•˜๊ณ , ๋ช…๋ น์„ ๋ถ€์—ฌํ•œ ๋’ค ์˜ˆ์‹œ๋ฅผ ์ œ๊ณตํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • call() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์‘๋‹ต์„ ๋ฐ›๊ณ , content() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์‘๋‹ต ๋ณธ๋ฌธ์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. (String)
  • BeanOutputConverter์˜ convert ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด PredictFoodSearchResponse์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

๊ฒฐ๊ณผ

์ตœ์ข…์ ์œผ๋กœ ๋ฒ„๊ฑฐ ์‚ฌ์ง„์„ ์ฃผ๊ณ  ํ…Œ์ŠคํŠธํ•ด ๋ณด๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด dto์— ๋งž์ถ”์–ด ๋ฐ˜ํ™˜๋จ์„ ๋ณด์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

postman์—์„œ์˜ ์˜ˆ

 

์ด๊ฒƒ์„ ํ†ตํ•ด ์ด๋ฏธ์ง€ ๋ถ„์„, DTO ํŒŒ์‹ฑ, ํŒŒ์ผ ์ „์†ก ๋“ฑ์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ฝ์–ด์ฃผ์‹  ๋ถ„๋“ค ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค!

 

์ถ”๊ฐ€๋กœ ํ˜„์žฌ ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์ง์ ‘ text๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๋ถ€๋‹ด์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด ์Šคํ”„๋ง์˜ AOP๋ฅผ ์ด์šฉํ•˜์—ฌ, ํŒŒ์ผ (ํ”„๋กฌํ”„ํŠธ)์˜ ์ฃผ์†Œ๋งŒ ๋„ฃ์œผ๋ฉด ํ•ด๋‹น ํŒŒ์ผ์˜ ๋‚ด์šฉ์„ ์กฐํšŒํ•ด ์ฃผ๋Š” ๊ธฐ๋Šฅ์„ ๊ฐœ๋ฐœํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ถ€๋ถ„์€ AOP์™€ ๊ด€๋ จ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„์˜ ๊ธ€์—์„œ ๋‹ค๋ฃจ๊ฒ ์Šต๋‹ˆ๋‹ค.

 

Reference