팀 프로젝트/오늘하루를그려줘 : 2023.04 ~

응답 속도가 늦는 외부 API 호출 이슈 해결하기

밍 끄적 2023. 8. 4. 18:18
728x90

AI가 기똥차게 말아주는 그림일기 서비스, "오늘 하루를 그려줘" 앱에서 백엔드 개발을 맡고 있다.

https://github.com/tipi-tapi/ai-paint-today-BE

 

GitHub - tipi-tapi/ai-paint-today-BE: 🖼️ AI가 말아주는 오늘 하루의 그림 일기, "오늘 하루를 그려줘"

🖼️ AI가 말아주는 오늘 하루의 그림 일기, "오늘 하루를 그려줘" 🖼️. Contribute to tipi-tapi/ai-paint-today-BE development by creating an account on GitHub.

github.com

ios, 안드로이드 모두 출시를 완료해서 서비스 중이니 다들 놀라움을 느껴보면 좋겠다 !

( 그리고 주변에 홍보도 )


서비스 소개

핵심 기능은 일기 생성이다. 사용자가 감정과 일기 내용을 입력하면, 그림 일기를 생성해서 제공한다.

그 외에도 일기 조회, 월별 일기 조회, 관리자 용 일기 조회, 로그인 등의 기능을 개발해둔 상태다.

 

그리고 AI를 통해서 이미지를 생성하는데, Open AI의 DALL-E 서비스를 API 호출하여 사용하고 있다.

 

사용자는 일기 생성을 최종적으로 요청하면, 약 9초 뒤에 생성된 그림 일기를 조회할 수 있다. 

그동안 사용자는 광고를 시청하거나 로딩 화면에서 대기한다.

 

아키텍처 소개

아래는 현재 백엔드 서버의 배포 파이프라인, 서버 아키텍처다.

 

CDN도 넣고, ECS도 넣고, ECR도 넣고 할 수 있지만, 현재 동아리에서 aws 지원을 받고 있기 때문에 최대한 가성비있게 활용하기 위해서 불필요한 인프라 구축은 제외했다.

 

도식화에서 DALL-E API 등은 편의상 제외하였다.

 


이슈 소개 - 일기 생성

현재 일기를 생성하는 구조에 문제가 있다. ( Github Issue )

 

아래는 일기를 생성하는 플로우다.

1. 사용자가 입력한 감정과 일기 내용을 통해, 프롬프트 문자열을 만든다.

2. 그 프롬프트 문자열을 DALL-E API에 요청해서 그림 일기의 이미지를 생성한다.

3. 일기 데이터 등을 DB에 저장한다.

4. 이미지를 S3에 업로드한다.

 

여기서 2번 과정에서 DALL-E API를 호출할 때, 평균적으로 약 8초 정도의 응답 시간이 소요된다.

 

그래서 클라이언트는 일기 생성 API를 요청하면 응답까지 9초간 대기한다.

만약 DALL-E API를 호출할 때, 다른 설정 값으로 호출한다면,
( ex. 생성할 이미지의 수를 증가시키거나, 화질을 더 높은 화질을 사용하거나) 시간은 더 오래 소요된다.

 

위험성

현재 API 서버는 큰 문제 없이 동작하고 있지만 잠재적인 위험이 있다.

 

1. 동시에 일기 생성을 요청할 경우

  • 서비스 이용자 규모가 크지 않지만,
    여러 유저가 일기 생성 요청을 보낼 경우, 서버에 동시 처리 요청이 누적하며 증가하게 된다.
  • 이러한 요청이 일정 수를 초과할 경우, 일기 생성이 아닌 일반 조회 API 요청 등도 처리할 수 없다.

2. DALL-E API와 상관없이 DB로부터 조회하고 쓰는 것도,
동시에 여러 요청이 들어올 경우 정상적으로 요청을 처리하지 못할 수 있다.

  • 이 문제는 DB 커넥션 풀을 늘리면 일시적으로는 해결할 수 있지만, 메모리 부족등의 또다른 문제를 고려해야한다.

 

3. 분당 DALL-E API 요청량을 제어하지 못한다.

  • 현재 사용하고 있는 DALL-E API 플랜은 분당 요청 횟수가 50회다.
  • DALL-E API 무료 플랜의 경우에는 분당 요청 가능 횟수가 더 낮다.
  • 아직까지는 분당 50번을 초과할 일이 없지만, 확장성을 생각해서 Rate Limit 처리가 필요하다.

아키텍처 개선 - 비동기 처리

그래서 아키텍처의 개선이 필요하다.

이 DALL-E API가 속도가 개선되면 참 좋겠지만, 그렇게 처리하기 어렵기 때문에 이 외부 API 호출을 비동기적으로 처리하게 바꿔야한다.

 

그래서 비동기적으로 어떻게 호출할까 고민해봤다.


1. Spring WebFlux : 탈락

Spring WebFlux는 MVC 와 달리 비동기 Nonblocking 으로 동작한다.

 

그래서 인프라 상으로, 앞단에 WebFlux 서버를 위치시킨다.

일기 생성은 WebFlux로 처리하고, 그 외의 처리는 MVC 서버가 처리하도록 하는 구조를 생각했다.

 

매우 좋은 방법이라고 생각한다! 사실 여유가 있다면 가장 권장되는 방법이라고 생각한다.

 

1-1. 그럼에도 불구하고 사용하지 못하는 이유는, 첫번째로 인프라가 매우 복잡해진다.

 

WebFlux에서 처리하는 도메인과 MVC가 처리하는 도메인이 일치하게 되는데, 현재 프로젝트는 각각의 팀이 존재하는 것이 아니다.

그래서 같은 팀이, 같은 사람이 하나의 도메인을 중복으로 관리하게 된다. 

 

( 여담으로, 이걸 인턴 근무하면서 경험했었다. 같은 도메인을 쓰는데 하나는 실험적으로 사용하는 서버-개발이나 테스트 서버 아님, 하나는 사용하고 있는 서버였다. 복잡하고 휴먼 에러도 발생하기 쉬웠다. 웬만한 체계가 확립되지 않는 이상 어려울 것 같다. 참고로, 이때는 Spring이 아닌 Node.js 였다. )

 

이 부분도 해결책이 있다. ( Thanks to tigercow... )

중복되는 도메인 부분 등을 공통 모듈으로 처리해서 해결하는 방법이다.

 

그럼 여기까지 WebFlux + MVC + 공통 모듈의 구조다. 복잡한 구조다.

+ 우리 백엔드 팀은 WebFlux, 공통 모듈을 모른다. 특히 내가 모른다!!! 

+ 현재 MVC로 필요한 기능이 다 구현되어있기에 리팩토링..만 하면 되긴하는데 일단 동아리 종료가 1달도 안남았다. ( 글 작성 기준 20일 쯤 남음 )

물론 계속 서비스 개발에 참여하겠지만 다른 신규 기능도 개발해야하고, 다른 좋은 방안이 있을 것 같다.

 

1-2. MySQL 공식 지원 드라이버가 없다.

PostgreSQL은 공식 지원 드라이버가 있는데, MySQL은 없다.

그런데 우리 서비스는 현재 MySQL을 쓰고있다.


2. Async 처리 : 탈락

Spring MVC에서도 @Async 처리를 통해서 비동기 처리할 수 있다.

할 수는 있는데 역시 문제가 있다.

 

2-1. public 메서드에만 적용할 수 있다.

2-2. 같은 클래스 내의 메서드를 호출할 수 없다. (self-invocation 불가)

이런 두개의 제약 뿐만 아니라

 

2-3. 트랜잭션과 스레드가 분리된다.

트랜잭션이 있는 영역에서 async 메서드를 호출하면, async는 트랜잭션이 있는 스레드가 아니라 별도의 스레드에서 수행된다.

그러면 Unchecked Exception이 발생해도, 롤백이 안된다. 안타깝게도, 우리는 DALL-E API 호출에 실패하면 실패한 데이터를 남겨야 한다.

 

2-4. Filter, Interceptor 자체가 동기 처리라 엄청난 성능 향상은 없다.

 

Async나 Spring Event에 대한 깊은 이해가 된 상태가 아니라서, 정확하지 않을 수 있다! ( 댓글로 제발 알려주세요 ㅈ베ㅏㄹ 제발요 )

그리고 Spring Event를 써도 비동기 처리하려면 Async를 써야한다.


3. Node.js 사용 : 준 탈락

이것은 사실 Node.js의 옛 애증을 담은 나의 의견이다.

Node.js는 싱글 스레드지만, 내부적으로 Event Queue에 이벤트를 적재해두고, Event Loop를 통해서 동기적인 이벤트를 처리하고, 비동기 작업은 C언어 라이브러리 libuv 기반의 스레드 풀에서 처리한다.

그래서 외부 API 호출을 비동기적으로 처리할 수 있다.

그러니까 외부 API 호출은 스레드 풀에서 담당하기 때문에 메인 스레드가 Blocking 되지 않으면서 다른 요청을 처리할 수 있다.

 

사실 이것도 Webflux 해결방안에서 만난 문제가 직면한다. 중복적인 코드 이슈다.

나의 Node.js 사랑으로 이겨내볼까도 했지만, 우선 Node.js는 나만 안다. 다른 팀원은 Node.js 경험이 없기 때문에 최선의 방법은 아니다.

 

하지만 최종적으로 고려하고 있는 방안에서 Node.js가 재등장 하게 된다!


4. DALL-E API 웹 훅 사용 : 탈락

웹훅으로.. 이미지 생성 완료를 전달받는 다던가 하면 좋을텐데

DALL-E API 웹훅을 쓸 수가 없다. 왜냐면 지원하지 않는다. ( 있었는데? 아니 없어요 그냥 ) 


5. 메시지 큐 사용 : 최종 고려 방안

일기 생성에 대한 요청을 큐에 보관하고, 큐에서 정책에 따라 별도의 서버로 메시지(요청)을 전달한다.

 

그러면 아래와 같은 이점을 취할 수 있다.

1. 일기 생성에 대한 성공 실패 처리가 유연해진다.

  • 현재 DALL-E 요청에 실패하면, 다른 방향으로 DB에 저장해야해서 롤백하지 않는다. ( 어떤 프롬프트가 실패하는 프롬프트인지 검증하고자, 저장한다. )
  • 그런데 그 이후에 일기 생성에 실패하면, 데이터를 저장하지 않는다.
  • 메시지큐 + 별도 서버를 쓰면, 별도 서버에서 DALL-E 요청 실패했을 경우에는 원래 서비스 API 서버에 다른 API를 요청할 거라서 성공, 실패 처리가 깔끔해진다.

2. DALL-E API에 대한 요청량을 조정할 수 있다.

  • 큐에서 Consumer 서버로 보낼 양을 조정함으로써 수행할 수 있다.

3. 일기를 생성하는데에 소요되는 평균 9초의 대기시간이 필요 없다. 유저는 알림을 기다리면 된다.

  • 하지만 그래서 기획은 바뀌어야한다. 이게 제일 문제라서 당장 도입하지 못하고 있다.

4. DALL-E API가 아닌 Stable-Diffusion API 등 다른 외부 이미지 생성형 AI 서비스를 쓰게되더라도 실 서비스의 영향이 거의 없다.

  • 별도 서버에만 코드를 수정하면 되서 서비스 서버에 영향이 없다.

5. 당연히, DALL-E API에 대한 요청을 비동기적으로 처리해, 다른 서비스에 영향이 가지 않도록 처리할 수 있다.

 


어떻게 구체적으로 메시지큐 + 별도 서버를 둘 것인가

메시지 큐도 정말 후보가 많다.

RabbitMQ, Kafka, Redis, ...

이런 비동기 처리를 위해서는 Kafka가 좋다는데, 러닝 커브와 시간적 제약으로 인해서 어려운 방안이라는 판단을 내렸다.

 

1. 그래서 메시지 큐는 AWS SQS를 쓰고자한다.

 

UI로 관리할 수도 있고, java 지원도 된다.

아무래도 구축하는데 어려움이 비교적 적다는게 가장 큰 점인 것 같다.

 

비용적으로도 누적 100만건 메시지 처리까지는 무료라고 한다. ( 월별 100만건 아님 )

 

그럼 별도 서버는 어떻게 구축할 것인가?

어쨌든 분리되긴했으니까, Spring MVC로 그냥 냅다 띄울까? 당연히 아니다. 그리고 EC2 비용도 있다.

 

그리고 외부 API 호출이 핵심이다. 

그 외부 서버는 아래와 같이 운영되면 된다.

1. 메시지 큐로부터 API 호출

2. DALL-E API를 요청해서 이미지 데이터 받기

3. DALL-E API의 처리 성공 / 실패에 따라 서비스 API의 일기 생성 API / 일기 생성 실패 API를 호출한다.

4. 처리가 완료됨을 클라이언트에 알린다. : ex ) FCM

 

rest 호출을 잘 하면 되고, 도메인 관리할 필요도 없다.

 

2. 그래서 DALL-E API로 요청하는 별도 서버는 AWS Lambda 를 쓰고자 한다.

 

무겁지 않은 Task를 실행하기에 적합하다.

비용적으로도 월별 백만회 요청이 무료다. ( 참고로 여기에 한 달에 400,000GB-초의 컴퓨팅 시간 제약이 있다. )

 

java를 사용하는건 좀 어렵지만, node.js나 python을 쓸 수 있다.

이 정도 node.js 사용은 어렵지 않기도 하고, 내가 익숙하기도 하고, rest 요청이 포함되기도 하니까 node.js를 사용하고자 한다.

 

또, SQS + Lambda 조합은 레퍼런스도 많다.


최종적으로 고려하고 있는 아키텍처

사용하는 서버 / 리소스는 6개로, 도식화한 아키텍처에서는 아래 아이콘으로 표현했다.

 

 

아래는 아키텍처를 도식화한 것이다.

 


고려해야할 점

위 방안은 확정된 방안이 아니다.

1. 기획적으로 로딩 -> 알림으로 바꿔야한다.

기획이 변경되면 서버도 서버지만 클라이언트 측의 변경도 크다.

2. 알림을 어떻게 전달할 것인지 구체적으로 클라이언트와 논의해야한다.

Flutter 앱이라서, 같은 생태계인 Firebase 알림 서비스 FCM을 쓰면 편리하지 않을까 마냥 생각했는데 딱히 그런 이점이 있는 것 같지는 않았다.

게다가 FCM의 수신률이 100%가 아니라는 이슈가 있다고 한다.

AWS SNS나 다른 방안을 찾고, 논의할 필요가 있다.

 

 

기획적인 변경이나, 더 많은 클라우드 리소스를 쓴다는 점이나 현재 유저가 많지 않은 상황에서 무리한 확장일 수 있다는 관점도 있기 때문에 아직 확정할 수는 없다.

 

우선은 성능테스트를 진행해서 우리 서버의 한계를 시험할 예정이다.

 

끝.

피드백 언제나 팔벌리고 환영입니다. 

피드백 주시는 모든 분들 적게 일하고 돈 엄청 많이 버실거에요..🥰

그리고 도움을 주신 tigercow 정말 감사합니다 :D

728x90