https://github.com/Hot6-NovelCraft/Hot6-NovelCraft
GitHub - Hot6-NovelCraft/Hot6-NovelCraft
Contribute to Hot6-NovelCraft/Hot6-NovelCraft development by creating an account on GitHub.
github.com
개인별 배포용 레퍼지토리 [ AWS ]
https://github.com/MinWoo1995/Hot6-NovelCraft-local
GitHub - MinWoo1995/Hot6-NovelCraft-local
Contribute to MinWoo1995/Hot6-NovelCraft-local development by creating an account on GitHub.
github.com
1. 오늘 한 일
오늘은 NovelCraft 프로젝트에서 AI 도메인 중 소설 표지 생성 기능을 담당하여 OpenAI DALL-E 3 API를 Spring Boot에 처음으로 도입했다.
- AI 도메인 패키지 구조 설계 (ai/cover 독립 패키지)
- OpenAI Java SDK 의존성 추가 및 API 키 설정
- DallEClient 구현 (DALL-E 3 API 호출 전담)
- CoverService 구현 (소설 정보 DB 조회 → 프롬프트 자동 생성 → 이미지 생성)
- CoverController 구현 (POST /api/ai/novels/{novelId}/cover)
- CoverExceptionEnum 구현 (공통 예외 패턴 적용)
- Postman으로 로컬 테스트 완료 (DALL-E 응답 성공 확인)
2. 트러블슈팅
2-1. Optional.get()에 인덱스 인자를 넣어 컴파일 에러 발생
문제 상황
DallEClient에서 DALL-E 응답의 이미지 URL을 꺼내는 코드를 아래와 같이 작성했다.
return response.data().get(0).url()
.orElseThrow(() -> CoverExceptionEnum.IMAGE_GENERATION_FAILED.toException());
컴파일 시 아래 에러가 발생했다.
method get in class Optional<T> cannot be applied to given types;
required: no arguments
found: int
원인 분석
OpenAI Java SDK에서 response.data()의 반환 타입이 List<Image>가 아닌 Optional<List<Image>>였다. Optional의 get()은 인자를 받지 않는데 인덱스 0을 넣어버리니 타입 불일치 에러가 발생한 것이다.
해결
response.data() 자체가 Optional이므로 먼저 orElseThrow()로 꺼낸 뒤 리스트 인덱스로 접근하도록 수정했다.
return response.data()
.orElseThrow(() -> CoverExceptionEnum.IMAGE_GENERATION_FAILED.toException())
.get(0)
.url()
.orElseThrow(() -> CoverExceptionEnum.IMAGE_GENERATION_FAILED.toException());
SDK를 처음 사용할 때는 반환 타입을 IDE에서 직접 확인하는 습관이 필요하다는 것을 느꼈다.
2-2. S3 업로드 403 에러 (The request signature does not match)
문제 상황
DALL-E 호출은 성공했지만 이후 S3 업로드 단계에서 아래 에러가 발생했다.
S3Exception: The request signature we calculated does not match the signature you provided.
Status Code: 403
원인 분석
로컬 테스트 환경에서 S3 액세스 키 권한 문제로 서명 불일치가 발생한 것이었다. DALL-E가 반환하는 임시 URL을 S3에 업로드하는 흐름이었는데, 로컬에서는 S3 연동 자체가 불필요한 상황이었다.
해결
로컬 테스트 단계에서는 S3 업로드 없이 DALL-E가 반환한 임시 URL을 그대로 반환하도록 수정했다. DALL-E URL은 1시간 후 만료되지만 로컬 테스트 목적으로는 충분하다. 실제 배포 시에는 S3 업로드 로직을 복구할 예정이다.
public CoverCreateResponse generateCover(Long novelId) {
Novel novel = novelRepository.findByIdAndIsDeletedFalse(novelId)
.orElseThrow(() -> CoverExceptionEnum.NOVEL_NOT_FOUND.toException());
String prompt = buildPrompt(novel);
String imageUrl = dallEClient.generateImage(prompt); // DALL-E URL 바로 반환
return CoverCreateResponse.of(novelId, imageUrl);
}
3. 주요 구현 내용
패키지 구조
ai/cover/
├── controller/ CoverController
├── service/ CoverService
├── client/ DallEClient
├── dto/
│ ├── request/ CoverCreateRequest
│ └── response/ CoverCreateResponse
└── exception/ CoverExceptionEnum
프롬프트 자동 생성 전략
클라이언트에서 별도 요청 파라미터 없이 novelId만 받아서 DB에서 소설 정보를 조회한 뒤 프롬프트를 자동으로 구성했다. no text, no letters 지시어를 반드시 포함해야 이미지에 깨진 글자가 생성되지 않는다는 점을 알게 됐다.
private String buildPrompt(Novel novel) {
return String.format(
"Create a professional Korean novel cover image. " +
"Title: %s, Genre: %s. " +
"Story summary: %s. " +
"Style: cinematic, high quality book cover art, no text, no letters.",
novel.getTitle(),
novel.getGenre(),
novel.getDescription()
);
}
API 엔드포인트
POST /api/ai/novels/{novelId}/cover
Authorization: Bearer {JWT}
→ 응답
{
"success": true,
"status": "200",
"message": "소설 표지가 성공적으로 생성되었습니다",
"data": {
"novelId": 1,
"coverImageUrl": "https://..."
}
}
4. 느낀 점
AI API를 Spring Boot에 처음 도입해봤는데 생각보다 구조 자체는 단순했다. 외부 API 클라이언트를 별도 client 레이어로 분리하니 서비스 코드가 깔끔하게 유지됐다.
DALL-E 3는 응답 시간이 평균 20초 이상 걸린다는 점이 인상적이었다. 실서비스라면 비동기 처리나 로딩 UX가 반드시 필요하다는 걸 체감했다.
이미지에 제목이나 작가명을 자연스럽게 넣는 것은 DALL-E만으로는 한계가 있다는 점도 알게 됐다. 실제 서비스에서는 배경 이미지 생성 후 프론트엔드에서 텍스트를 합성하는 방식이 가장 현실적이라는 결론을 팀원들과 공유했다.
AI 기능은 구현 자체보다 어떤 프롬프트를 어떻게 구성하느냐가 품질을 좌우한다는 것을 느꼈다. 앞으로 프롬프트 엔지니어링도 백엔드 개발자가 알아야 할 중요한 역량이 될 것 같다.
'spring_2기[본캠프] > 과제' 카테고리의 다른 글
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 17 (0) | 2026.05.08 |
|---|---|
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 16 (0) | 2026.05.07 |
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 14 (0) | 2026.05.04 |
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 13 (0) | 2026.05.01 |
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 12 (1) | 2026.04.30 |