spring_2기[본캠프]/과제

[파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 15

minwoo95 2026. 5. 6. 22:03

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을 꺼내는 코드를 아래와 같이 작성했다.

 
 
java
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()로 꺼낸 뒤 리스트 인덱스로 접근하도록 수정했다.

 
 
java
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 업로드 로직을 복구할 예정이다.

 
 
java
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 지시어를 반드시 포함해야 이미지에 깨진 글자가 생성되지 않는다는 점을 알게 됐다.

 
 
java
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 기능은 구현 자체보다 어떤 프롬프트를 어떻게 구성하느냐가 품질을 좌우한다는 것을 느꼈다. 앞으로 프롬프트 엔지니어링도 백엔드 개발자가 알아야 할 중요한 역량이 될 것 같다.