해커톤 'Plan Death'를 하며, 우리팀은 기술 점수를 갖고 가고 싶었다. 그래서 이것저것 찾아보다가 내린 결론이 'AI를 활용하자'이다. 아무래도 삶의 가치를 느끼게끔 하는 프로젝트다 보니 좋았던 시절을 회상할 수 있는 서비스를 기획하게 되었고, 이 좋은 시절을 회상하는 것을 넘어서 그림으로 보여주고 싶다는 생각이 들어 생성형 ai 서비스를 활용하게 되었다.
요즘 인스타에서 ai를 활용하여 고퀄리티 그림을 만들어 내는 사람들이 많이 뜬다. 그 사람들 대부분이 Chat GPT를 활용한다는 답변을 받아, 우리팀도 Dalle를 사용하기로 결론을 내렸다.

코드의 구조는 다음과 같다.

우선, 코드는 git을 참고해서 우리 팀의 입맛에 맞게 변동하였으나, open ai에 나와있는 공식문서를 참고하여 request 요청을 해 주었다.
OpenAI의 api를 사용하기 위한 설정을 하는 부분
@SpringBootApplication
@ConfigurationPropertiesScan
public class DallecoolApplication {
private static final Logger LOGGER = LoggerFactory.getLogger(DallecoolApplication.class);
public static void main(String[] args) {
SpringApplication.run(DallecoolApplication.class, args); //클래스 실행
}
@Bean
CommandLineRunner onStart(OpenAiConfiguration openai) {
return new CommandLineRunner() {
@Override
public void run(String... args) throws Exception { //이벤트 기록
LOGGER.info("Using OpenAI base URL: {}", openai.api());
LOGGER.info("Loaded OpenAI key: {}", openai.key().replaceAll(".", "*"));
}
};
}
}
@RestController
@ControllerAdvice
public class DallecoolController {
private static final Logger LOGGER = LoggerFactory.getLogger(DallecoolController.class);
private final DalleImageGeneratorService imageGenerator;
public DallecoolController(DalleImageGeneratorService imageGenerator) {
this.imageGenerator = imageGenerator;
}
@GetMapping("/api/v1/image") //prompt값을 받아옴
Mono<ImageResponse> generateImage(@RequestParam("prompt") String prompt) {
return imageGenerator.generateImage(prompt).map(resp -> new ImageResponse(prompt, resp));
}
@ExceptionHandler(WebClientException.class)
ProblemDetail onError(WebClientException e) {
LOGGER.warn("Error while generating image with DALL-E", e);
final var problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
problemDetail.setTitle("Image generation failed");
problemDetail.setType(URI.create("urn:problem-type:image-generation-failed"));
return problemDetail;
}
@ExceptionHandler(IllegalArgumentException.class)
ProblemDetail onError(IllegalArgumentException e) {
LOGGER.warn("Input error while generating image with DALL-E", e);
final var problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());
problemDetail.setTitle("Input error");
problemDetail.setType(URI.create("urn:problem-type:input-error"));
return problemDetail;
}
public record ImageResponse(String prompt, String url) {
}
}
@Service
public class DalleImageGeneratorService { //달리와 통신 + 이미지 생성에 사용
private static final Logger LOGGER = LoggerFactory.getLogger(DalleImageGeneratorService.class);
private final OpenAiConfiguration openai;
private final WebClient client;
private final String apiEndpoint = "/v1/images/generations";
public DalleImageGeneratorService(OpenAiConfiguration openai, WebClient client) {
this.openai = openai;
this.client = client;
}
public Mono<String> generateImage(String prompt) { //프롬프트를 통해 달리에 이미지 생성 요청 전송
if (!StringUtils.hasText(prompt)) {
throw new CustomException(ErrorCode.VALIDATION_REQUEST_MISSING_EXCEPTION, ErrorCode.VALIDATION_REQUEST_MISSING_EXCEPTION.getMessage());
}
LOGGER.info("Sending request to DALL-E: {}", prompt);
final var req = new ImageGenerationRequest(prompt,"dall-e-3");
return client.post().uri(openai.api() + apiEndpoint)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + openai.key())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.bodyValue(req)
.retrieve()
.bodyToMono(ImageGenerationResponse.class)
.doOnSuccess(resp -> {
LOGGER.info("Received response from DALL-E for request: {}", prompt);
})
.filter(resp -> resp.data.length > 0)
.map(resp -> resp.data[0].url);
}
private record ImageGenerationResponse(ImageGenerationResponseUrl[] data) {
}
private record ImageGenerationResponseUrl(String url) {
}
private record ImageGenerationRequest(String prompt, String model) {
}
}
public class OpenAiBindingsPropertiesProcessor implements BindingsPropertiesProcessor {
public static final String TYPE = "openai";
@Override
public void process(Environment environment, Bindings bindings, Map<String, Object> properties) { //openai와 관련된 바인딩에서 시크릿 키와 api를 가져와 속성 맵에 추가 -> 설정 정보 자동 로드
bindings.filterBindings(TYPE).forEach(binding -> { //필터링
properties.putIfAbsent("openai.key", binding.getSecret().get("key"));
properties.putIfAbsent("openai.api", binding.getSecret().get("api"));
});
}
}
@Validated
@ConfigurationProperties(prefix = "openai") //openai가 접두사인 속성 값을 읽음
public record OpenAiConfiguration(@NotEmpty String key, @NotEmpty String api) {
//api키와 endpoint를 구성
}
@Configuration
public class WebClientConfiguration {
@Bean
WebClient.Builder webClientBuilder() { //webClient를 설정하고 구성
return WebClient.builder()
.defaultHeader(HttpHeaders.USER_AGENT, "dallecool") //모든 http 요청에 대한 기본 사용자 에이전트 헤더를 dallecool로 설정
.clientConnector(new ReactorClientHttpConnector(HttpClient.create().proxyWithSystemProperties())); //프록시 구성
}
@Bean
WebClient webClient(WebClient.Builder clientBuilder) { //webClient 인스턴스 생성
return clientBuilder.build();
}
}