교내 탄소중립 SW 아카데미 4기에서 탄소중립 뉴스 데이터를 시각적으로 보여주기 위해 카드뉴스 형식의 콘텐츠를 자동 생성하는 프로젝트를 진행중이다. 이 과정에서 OpenAI의 GPT 및 이미지 생성 API, 그리고 Python의 Pillow 라이브러리를 결합하여 글자 삽입이 가능한 카드뉴스를 완성하였다. 카드뉴스 자동화 개발까지 시행착오를 포스팅하고자 한다.
Open AI API 활용
OpenAI API 만으로 카드뉴스 만들기
패키지 구조
cardNews-spring
└─ src
└─ main
├─ java
│ └─ com.swacademy.demo
│ ├─ controller
│ │ └─ NewsController.java
│ ├─ dto
│ │ ├─ NewsRequestDto.java
│ │ └─ NewsResponseDto.java
│ └─ service
│ ├─ OpenAIClient.java
│ ├─ NewsSummaryService.java
│ ├─ ImageGenerationService.java
│ └─ NewsService.java
└─ resources
└─ application.yml
- controller: 클라이언트 요청을 처리하는 REST API 엔드포인트
- dto: 요청과 응답을 위한 데이터 전송 객체
- service: OpenAI API 호출, 요약·이미지 생성, Python 오버레이 연동 로직
OpenAIClient 설정
OpenAIClient
는 WebClient를 이용해 OpenAI의 ChatGPT·DALL·E 엔드포인트를 호출한다.
application.yml
에 API 키를 설정하고 @Value
로 주입한다.
@Component
public class OpenAIClient {
private final WebClient webClient;
public OpenAIClient(@Value("${openai.api.key}") String apiKey) {
this.webClient = WebClient.builder()
.baseUrl("https://api.openai.com/v1")
.defaultHeader("Authorization", "Bearer " + apiKey)
.build();
}
public String summarize(String title, String content) {
String prompt = String.format("뉴스 제목: %s\n본문: %s\n이 내용을 카드뉴스용으로 3~4줄로 요약해줘.", title, content);
Map<String, Object> body = Map.of(
"model", "gpt-4o",
"messages", List.of(Map.of("role", "user", "content", prompt))
);
return webClient.post()
.uri("/chat/completions")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(body)
.retrieve()
.bodyToMono(JsonNode.class)
.map(json -> json.at("/choices/0/message/content").asText())
.block();
}
public String generateImage(String prompt) {
Map<String, Object> body = Map.of(
"model", "dall-e-3",
"prompt", prompt,
"n", 1,
"size", "1024x1024"
);
return webClient.post()
.uri("/images/generations")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(body)
.retrieve()
.bodyToMono(JsonNode.class)
.map(json -> json.at("/data/0/url").asText())
.block();
}
}
요약 및 이미지 생성 서비스
- NewsSummaryService:
OpenAIClient.summarize()
호출 - ImageGenerationService: 요약 결과에 어울리는 배경 이미지를 생성하는 DALL·E 프롬프트 작성 후
OpenAIClient.generateImage()
호출
@Service
public class NewsSummaryService {
private final OpenAIClient client;
public String summarize(String title, String content) {
return client.summarize(title, content);
}
}
@Service
public class ImageGenerationService {
private final OpenAIClient client;
public String generateImage(String summary) {
String prompt = String.format(
"프롬프트 내용",
summary
);
return client.generateImage(prompt);
}
}
REST API 엔드포인트
@RestController
@RequestMapping("/api/card-news")
public class NewsController {
private final NewsService service;
public NewsController(NewsService s) { this.service = s; }
@PostMapping
public NewsResponseDto create(@RequestBody NewsRequestDto req) {
return service.generateCardNews(req);
}
}
- 요청 예시
{ "title": "폭염 경고", "content": "지난해 한국 바다는 고수온 현상을 겪었다..." }
- 응답 예시
{ "title": "폭염 경고", "summary": "지난해 한국 바다는 관측 이래 최고 수온을 기록했다.", "imageUrl": "OpenAI API로 생성된 카드뉴스 이미지 Url" }
OpenAI API만 사용할 경우 발생한 한계
처음에는 모든 작업을 OpenAI API만으로 처리하고자 하였다.
GPT로 요약된 텍스트를 받아, DALL·E 3를 통해 카드 형태의 이미지(백그라운드 + 문구 포함)를 바로 생성하려고 하였다.
이를 위해 다음과 같은 영어 프롬프트를 사용하였다.
"Create an infographic in Korean, like a public awareness poster.\n" +
"The image should clearly show the following Korean text in bold black font:\n" +
"\"폭염은 생명을 위협하는 기후 위기입니다.\"\n" +
"Use a white or light gray background, with a heatwave visual (e.g., sun or dry earth). " +
"Make the Korean text look like a headline in a card-style poster."
하지만 결과는 기대와 달랐다.
DALL·E는 영문 텍스트는 일정 수준 표현할 수 있었지만, 한글 텍스트 처리에 매우 취약하였다.
폰트가 무너지거나 텍스트 인식 자체가 되지 않았고, 배경도 매번 랜덤하게 생성되어 일관된 카드 스타일을 만들기 어려웠다.
이로 인해 이미지와 텍스트를 분리 처리하는 방식으로 구조를 전환하게 되었다.
이미지 프롬프트 최적화 과정
카드뉴스의 배경 이미지는 여전히 DALL·E를 통해 생성하였다.
단, 텍스트는 제외하고 중앙 하단에 심플한 아이콘만 배치된 배경 이미지 형태로 제한하여 프롬프트를 수정하였다.
String imagePrompt = String.format(
"Create a simple and clean background image based on the provided summary: %s. " +
"Place a summary content relevant, minimal icon (such as a globe, plant, or tree) centered in the image, but positioned slightly below the center. " +
"Ensure that the icon is not framed, and it blends naturally into the background. " +
"Leave enough space at the top and bottom to maintain a balanced, clean layout. " +
"The design should focus solely on the central icon, without any text, and maintain simplicity and clarity.",
summary.replace("\n", " ")
);
이전 프롬프트에서는 실제 뉴스 문장을 그대로 이미지에 포함시키려 했으나, 위처럼 텍스트는 배제하고 아이콘 중심의 구성으로 바꾼 이후 훨씬 안정적인 이미지 결과를 얻을 수 있었다.
Pillow를 통한 텍스트 삽입 처리
텍스트 삽입은 Python의 Pillow 라이브러리를 활용하였다.
Spring 서버는 요약된 텍스트와 생성된 이미지 URL을 Python Flask 서버로 전송하고, Flask 서버는 해당 이미지를 다운로드한 후, 요약된 텍스트를 적절한 위치에 삽입하여 최종 카드뉴스 이미지를 생성한다.
Pillow를 사용함으로써 다음과 같은 커스텀이 가능하였다.
- 한글 폰트 지정 (
Pretendard
,Noto Sans
) - 문단 줄바꿈 및 위치 자동 조절
- 아이콘 중심 하단 배치 + 텍스트 상단 배치 형태로 레이아웃 구성
- 시각적 간결함을 유지하면서 가독성 확보
텍스트와 이미지가 명확히 분리되었기 때문에, 레이아웃이나 색상 조정도 자유도가 높았고, 이미지 디자인을 코드로 제어할 수 있어 테스트가 수월하였다.
전체 시스템 구성 흐름
최종적으로 구성한 시스템은 다음과 같은 흐름을 따른다.
- Spring 서버가 주기적으로 뉴스 기사를 크롤링하고 DB에 저장한다.
- 저장된 뉴스 본문을 기반으로, GPT를 통해 요약과 이미지 프롬프트를 생성한다.
- GPT 응답 중 요약문과 이미지 프롬프트를 활용하여 DALL·E API로 배경 이미지를 생성한다.
- 해당 이미지와 텍스트를 Flask 서버로 전달하고, Pillow로 텍스트가 삽입된 최종 카드뉴스 이미지를 생성한다.
- 완성된 이미지를 S3에 저장하고, 해당 URL을 DB에 저장하여 클라이언트에 제공한다.
'Server > Spring' 카테고리의 다른 글
[Spring/프로젝트] Process Builder로 Python 코드 스케줄링 (0) | 2025.05.11 |
---|---|
[Spring/공부] Spring JPA 고급 활용 (JPQL, Query DSL) (0) | 2024.12.05 |
[Spring/공부] Spring JPA와 프로젝트 구조 (0) | 2024.11.20 |
[Spring/공부] Spring Boot 코어 개념 정리 (0) | 2024.11.05 |