개발 알다가도 모르겠네요

[Spring] SunoAI와 Spring Boot를 연동해보자 본문

웹/Spring

[Spring] SunoAI와 Spring Boot를 연동해보자

이재빵 2024. 5. 15. 22:19
728x90

이번 글에서는 SunoAI와 Spring Boot를 연동해 사용자 감정에 맞는 음악을 생성하는 기능을 구축하는 방법을 설명하겠다.

SunoAI는 AI를 활용해 음악을 생성하는 서비스로, 사용자의 감정에 맞춘 음악을 제공하는 데 적합하다. 그러나 SunoAI는 공식 API를 제공하지 않기 때문에 SunoAI의 쿠키값을 이용한 GitHub 오픈소스 API를 사용하였다. (EC2 서버에 따로 AI서버 구축한 상태)

이 기능은 일기를 작성한 후 ChatGPT를 이용해 일기의 핵심 단어를 추출하고, 이를 바탕으로 SunoAI가 감정에 맞는 음악을 생성하도록 설계했다.

 

GitHub - gcui-art/suno-api: Use API to call the music generation AI of suno.ai, and easily integrate it into agents like GPTs.

Use API to call the music generation AI of suno.ai, and easily integrate it into agents like GPTs. - gcui-art/suno-api

github.com

 

SunoAIConfig 클래스

SunoAI API와의 연동을 위해 설정 클래스를 작성한다. SunoAIConfig 클래스는 SunoAI API 호출에 필요한 RestTemplate과 HttpHeaders를 설정하고, SunoAI API의 URL을 설정 파일에서 읽어오기 위해 SunoAIProperties 클래스를 사용한다.

@Slf4j
@Configuration
public class SunoAIConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    public HttpHeaders httpHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        return headers;
    }

    @Getter
    @Setter
    @Component
    @ConfigurationProperties(prefix = "sunoai")
    public static class SunoAIProperties {
        private String url;
    }
}

 

MusicService 클래스

MusicService 클래스는 SunoAI API를 호출하여 음악을 생성하고, 데이터베이스에 저장하는 비즈니스 로직을 포함하고 있다.

음악을 생성하는 createMusic 메서드는 Diary 엔티티를 조회하고, SunoAI API를 호출하여 음악 ID 목록을 받아와 Music 엔티티를 생성하여 저장한다.

@Service
public class MusicService {
    private final SunoAIConfig sunoAIConfig;
    private final SunoAIConfig.SunoAIProperties sunoAIProperties;
    private final MusicRepository musicRepository;
    private final DiaryRepository diaryRepository;

    @Transactional
    public void createMusic(MusicCreateRequestDto musicCreateRequestDto) {
        Diary diary = diaryRepository.findById(musicCreateRequestDto.getDiaryId())
                .orElseThrow(() -> new IllegalArgumentException("Diary not found with id: " + musicCreateRequestDto.getDiaryId()));

        SunoAIRequestDto sunoAIRequestDto = new SunoAIRequestDto();
        sunoAIRequestDto.setPrompt(musicCreateRequestDto.getGenerationPrompt());

        List<String> sunoAIMusicIds = generateSunoAIMusic(sunoAIRequestDto);

        if (sunoAIMusicIds.size() >= 2) {
            Music music1 = new Music(diary, musicCreateRequestDto.getGenerationPrompt(), sunoAIMusicIds.get(0));
            Music music2 = new Music(diary, musicCreateRequestDto.getGenerationPrompt(), sunoAIMusicIds.get(1));

            musicRepository.save(music1);
            musicRepository.save(music2);
        } else {
            throw new MusicException("SunoAI에서 생성된 음악 ID의 수가 부족합니다.");
        }
    }

    public List<String> generateSunoAIMusic(SunoAIRequestDto sunoAIRequestDto) {
        HttpEntity<SunoAIRequestDto> requestEntity = new HttpEntity<>(sunoAIRequestDto, sunoAIConfig.httpHeaders());
        ResponseEntity<List<SunoAIResponseDto>> response = sunoAIConfig.restTemplate().exchange(
                sunoAIProperties.getUrl() + "/generate",
                HttpMethod.POST,
                requestEntity,
                new ParameterizedTypeReference<>() {});

        return response.getBody().stream()
                .filter(musicData -> "submitted".equals(musicData.getStatus()))
                .map(SunoAIResponseDto::getId)
                .collect(Collectors.toList());
    }
}

 

MusicController 클래스

MusicController 클래스는 API 엔드포인트를 제공한다. 이 컨트롤러는 음악 생성을 위한 generateMusic 메서드를 포함한다.

@RestController
@RequestMapping("/api/v1/music")
public class MusicController {
    private final MusicService musicService;

    public MusicController(MusicService musicService) {
        this.musicService = musicService;
    }

    @PostMapping("/generate")
    public ResponseEntity<ApiResponse<SunoAIGenerationResultDto>> generateMusic(@RequestBody SunoAIRequestDto sunoAIRequestDto) {
        try {
            List<String> songIds = musicService.generateSunoAIMusic(sunoAIRequestDto);
            SunoAIGenerationResultDto result = new SunoAIGenerationResultDto(songIds);
            return ResponseEntity.ok(ApiResponse.success("음악 생성 완료", result));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(ApiResponse.error("음악 생성 실패: " + e.getMessage()));
        }
    }
}

 

기술적 인사이트

  1. API 연동의 중요성: SunoAI는 공식 API를 제공하지 않으므로 SunoAI의 쿠키값을 이용한 GitHub 오픈소스 API를 사용해야 했다. 이 과정에서 API 연동의 중요성을 깨달았다. 다양한 예외 상황을 처리하는 방법을 학습하였고, API 연동이 서비스의 핵심 기능을 수행하는 데 얼마나 중요한지 실감했다.
  2. 트랜잭션 관리의 필요성: 음악 생성 과정에서 여러 엔티티를 생성하고 저장하는 부분에서 트랜잭션 관리는 필수적이었다. 이를 통해 데이터 일관성을 유지하고, 예외 발생 시 롤백 처리를 통해 안정성을 확보할 수 있었다.
  3. 비동기 처리의 가능성: 음악 생성과 같은 시간이 오래 걸리는 작업을 비동기로 처리하면 시스템 성능을 크게 향상시킬 수 있을 것으로 보인다. 현재는 동기 방식으로 구현돼 있어 일기 요약(GPT4) 3초 + 음악생성(Suno AI) 3초~4초, 약 7~8초 정도 걸리는 상황이다.
    향후 비동기(@Async) 처리를 도입하여 사용자 응답 시간을 줄이고, 시스템 자원을 효율적으로 사용할 계획이다.