티스토리 뷰
구현 배경
이번 졸업 프로젝트에서는 ‘COOKiT’이라는 요리 초보를 위한 보유 식재료 기반 맞춤 레시피 큐레이터 서비스를 개발하게 되었다.
요리 경험이 부족한 사용자들이 자신이 가진 재료로 어떤 요리를 할 수 있을지 쉽게 찾을 수 있도록, 재료 기반 레시피 추천 기능을 중심으로 설계를 진행했다.
레시피 추천 기능을 구현하면서, 단순히 텍스트로만 정보를 제공하는 것보다는 시각적인 이미지가 함께 제공될 때 사용자 이해도와 몰입도가 훨씬 높아질 수 있겠다는 생각이 들었다. 하지만 레시피마다 개별적으로 이미지를 준비하는 것은 비효율적이라고 판단했고, 이에 따라 GPT와 DALL·E를 활용해 레시피에 맞는 이미지를 자동으로 생성하고 이를 AWS S3에 저장하는 방식을 도입하게 되었다.
이 글에서는 위 과정을 실제로 어떻게 구현했는지에 대해 단계별로 자세히 설명하고자 한다.
참고로, 이 과정에서 사용한 개발 프레임워크는 SpringBoot이다.
과정 설명
1. OpenAI Playground에서 API 키 생성하기
우선 OpenAI의 GPT와 DALL·E를 사용하려면 API 키가 필요했다. OpenAI 플랫폼에 접속해서 회원가입 후 로그인한 뒤, API keys 페이지에서 새로운 키를 발급받았다. 이 키는 나중에 서버에서 사용할 예정이라 별도의 안전한 곳에 보관했다.
2. GPT로 레시피 추천받기
우선, 모델은 가격과 성능을 모두 고려하여 가장 적합하다고 생각된 "gpt-4o-mini" 모델을 사용했다.
먼저, 사용자가 선택한 재료를 기반으로 GPT에게 3가지 레시피 추천을 요청했다.
처음에 간단히 프롬프트를 작성했을 때는 원하는 형식의 답변을 받기 어려웠다. 그래서 GPT가 정확한 JSON 형태로만 답을 주도록 프롬프트를 구체화했다.
구체화한 프롬프트는 다음과 같다.
private String generateRecipePrompt(String ingredients) {
return String.format(
"""
너는 프로 요리사야.
%s가 포함된 요리 3개를 JSON 형식으로 추천해줘.
각 레시피에는 반드시 모든 재료가 포함되어야 해.
각 재료의 적절한 양(단위 포함)도 반드시 포함해야 해.
**응답 형식 (JSON)**
```json
{
"recipes": [
{
"name": "요리 이름",
"ingredients": ["연어 200g", "새우 200g", "감자 2알"],
"time": 30,
"calorie": 500,
"difficulty": 3,
"steps": ["Step 1. 재료를 준비한다.", "Step 2. 연어를 열심히 볶는다.", "Step 3. 먹는다."]
},
{
"name": "다른 요리",
"ingredients": [...],
"time": ...,
"calorie": ...,
"difficulty": ...,
"steps": [...]
}
]
}
```
위 JSON 형식으로 **정확하게** 3개의 레시피 응답해줘.
모든 레시피에는 반드시 [%s]가 포함되어야 해.
JSON 외에 불필요한 문장은 절대 포함하지 마.
""", ingredients, ingredients);
}
위와 같은 로직을 통해 포스트맨(테스트 툴)을 이용해 요청을 보내면 아래와 같은 응답을 준다.
COOKiT의 레시피 추천 기능은 레시피 이름과 이미지, 요리 난이도, 재료, 요리 시간, 칼로리, 상세 레시피를 제공해준다.
위 사진 속 image에 해당되는 부분은 DALL·E를 통해 직접 생성한 레시피 이미지이다.
어떻게 이런 이미지가 자동으로 생성되고 사용자에게 제공되는지, 그 과정을 아래에서 하나씩 설명해보겠다!
3. GPT를 활용해 레시피 이름 영어로 번역하기
처음에는 한글로 된 레시피 이름을 그대로 넣어서 원하는 이미지가 제대로 나오지 않는 문제가 있었다. 이는 DALL·E가 영어로 된 설명을 더 잘 이해한다는 점 때문이었다. 그래서 GPT를 이용해 먼저 레시피 이름을 영어로 변환한 후 DALL·E에게 요청하는 방식으로 수정했다.
만약, GPT가 '딸기 복숭아 스무디'를 추천해주었다면 이를 GPT를 이용해 다시 'Strawberry Peach Smoothie"로 변환한 다음, 이 영어 레시피명은 DALL·E로 넘겨 이미지 생성을 하는 로직이다!
public String translateToEnglish(String koreanPrompt) {
String gptPrompt = "Translate the following food name to English. Return only the translated food name without any additional text: " + koreanPrompt;
GptRequestDto request = GptRequestDto.of("gpt-4o-mini", gptPrompt);
GptResponseDto response = restTemplate.postForObject(apiURL, request, GptResponseDto.class);
return response.choices().get(0).message().content();
}
4. DALL·E로 레시피 이미지 생성하기
본격적으로 이미지를 생성해보자!
이미지 생성을 요청하는 백엔드의 요청, 응답 포맷은 open ai 공식 문서를 참고하였다.
https://platform.openai.com/docs/api-reference/images/create
위 3번 과정을 통해 영어로 변환된 레시피 이름을 가지고 DALL·E에 이미지 생성을 요청했다. 처음에는 이미지가 제대로 생성되지 않거나 이미지 URL이 비어있는 경우가 있어서 이를 대비한 예외 처리도 추가해주었다.
public String generateAndUploadImage(String recipeName) {
String translatedPrompt = translateToEnglish(recipeName);
DalleRequestDto request = DalleRequestDto.of(translatedPrompt);
DalleResponseDto response = restTemplate.postForObject(dalleApiUrl, request, DalleResponseDto.class);
if (response == null || response.data().isEmpty()) {
return "defaultImageURL";
}
String dallEImageUrl = response.data().get(0).url();
return uploadToS3(dallEImageUrl, recipeName);
}
생성된 이미지 결과는 다음과 같다. 생각보다 실제 음식 사진 같고, 성능이 좋아 놀랬다.
5. AWS S3에 이미지 저장해 영구 보관하기
DALL·E를 처음 연동했을 땐, 생성된 이미지의 URL을 그대로 프론트에게 전달하는 방식으로 구현했다. 그런데 이 URL은 유효 시간이 지나면 자동으로 만료되어 이미지가 더 이상 표시되지 않았다. 사용자 입장에선 당연히 서비스 오류처럼 보이기 때문에, 이미지 결과를 영구적으로 저장하고 안정적으로 제공할 수 있는 구조가 필요했다.
그래서 선택한 방법이 AWS S3였다. 이미지 URL로부터 이미지를 다운받은 뒤, S3에 직접 업로드하는 방식으로 구조를 바꿨다. 핵심은 외부 이미지 URL을 바이트 배열로 변환하고, 이를 S3에 업로드하는 작업이었다.
직접 구현한 uploadImageFromUrl 메서드는 다음과 같은 과정을 거친다.
public String uploadImageFromUrl(String directoryPath, String imageUrl, String fileName) throws IOException {
final String extension = getDalleFileExtension(imageUrl); // URL에서 확장자 추출
final String key = directoryPath + generateImageFileName(fileName, extension);
final S3Client s3Client = awsConfig.getS3Client();
byte[] imageBytes = downloadImage(imageUrl);
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentType("image/" + extension) // 올바른 MIME 타입 설정
.contentDisposition("inline")
.build();
s3Client.putObject(request, RequestBody.fromBytes(imageBytes));
return s3Client.utilities().getUrl(builder -> builder.bucket(bucketName).key(key)).toExternalForm();
}
여기서 핵심은 downloadImage라는 메서드를 통해 외부 이미지 URL을 바이트 배열로 변환하는 부분이다.
private byte[] downloadImage(String imageUrl) throws IOException {
URL url = new URL(imageUrl);
URLConnection connection = url.openConnection();
connection.setRequestProperty("User-Agent", "Mozilla/5.0");
try (InputStream in = connection.getInputStream()) {
return IOUtils.toByteArray(in);
}
}
이 과정을 통해 생성된 이미지를 recipes/ 디렉토리에 저장하고, 최종적으로는 퍼블릭 URL을 만들어 사용자에게 제공한다. 이렇게 해두면 이미지 만료 걱정 없이 언제든 접근할 수 있다.
구현 결과
Spring Boot 서버를 실행한 후, 사용자가 원하는 재료를 입력하면 GPT가 레시피를 추천하고, DALL·E가 그 레시피에 맞는 이미지를 생성하여 S3에 저장한 후 결과를 제공했다. 사용자는 추천받은 레시피를 이미지와 함께 보면서 요리를 따라할 수 있었다.
아래는 실제 서비스 화면에서 볼 수 있는 추천된 레시피와 이미지 결과이다.
이 과정을 통해 레시피 추천과 이미지 제공을 자동화할 수 있었고, 서비스 사용성도 크게 향상되었다. 프로젝트를 진행하면서 기술적 난관이 있었지만, 단계별로 문제를 해결해 최종적으로 만족스러운 결과를 얻을 수 있었다.
이번 기능을 구현하면서 AI API와 외부 저장소(S3)를 연동하는 전반적인 흐름을 익힐 수 있었다. 특히, 외부 API에서 오는 비정형 응답을 다루는 부분과, 네트워크 예외 상황에 대한 예외 처리의 중요성을 다시금 느꼈다. 단순히 기능 구현을 넘어서, 사용자 경험과 시스템 안정성을 고려한 개발 방식에 대해 고민할 수 있었던 값진 경험이었다.
- Total
- Today
- Yesterday
- 자바 스프링
- 커뮤니티
- 프론트엔드
- SQLD
- SQL
- 로깅
- DP
- 파이썬
- JPA
- EnumType.ORDINAL
- 웹MVC
- 준영속
- 스프링 북마크
- 웹 MVC
- 비영속
- SQL 레벨업
- 백준 파이썬
- 회원탈퇴
- 로그아웃
- 인텔리제이
- 북마크
- 백준
- 스프링부트
- 스프링
- 스프링 커뮤니티
- 자바
- 지연로딩
- elasticsearch
- 다이나믹 프로그래밍
- 영속
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |