장난감 연구소
[개발] 최근 프로젝트에서 고민한 문제들 본문
구름톤 딥다이브 백엔드 개발자 과정에서 팀 프로젝트를 진행하면서 고민한 문제를 정리해 보았습니다.
OpenAI API 분당 토큰 사용량 제한 문제
ResuMate라는 이름으로 개인의 경험을 쉽게 기록·관리하는 회고 작성 서비스를 개발하였습니다. 해당 서비스는 OpenAI API를 이용하여 회고 내용을 요약하고, 핵심 요소를 뽑아주는 기능을 포함하고 있었습니다.
이때 기능 특성상 회고 내용이 모두 입력으로 들어가기 때문에 각 요청마다 입력 토큰으로 4000~6000 토큰 또는 그 이상을 사용하였습니다.
OpenAI API는 각 계정의 티어(Tier)에 따라 사용량 제한이 달랐습니다. 크레딧을 많이 지불할수록 티어가 올라가 제한량도 올라가나, 무작정 비용을 늘리기보다 한도 내에서 방법을 찾고 싶었습니다.
문제 배경
회고 분석에 사용되는 OpenAI API는 분당 처리 토큰 제한이 존재하였습니다. 이로 인해 1회 분석에 4000 토큰 이상이 소모되는 사용 패턴에 따라 제한을 많이 받을 것으로 판단하였습니다. 실제 프롬프트 작성 및 개선 과정에서도 토큰 사용량 제한에 걸리기도 하였습니다.
해결 방안
기존에는 별도의 제한을 두지 않고 POST 요청을 보냈으나, 분당 3만 토큰 이상 사용하면 요청이 실패하였습니다. 따라서 토큰 버킷 알고리즘을 구현한 Bucket4j를 통해 분당 요청을 5회로 제한하였습니다.
그러나 이로 인해 분당 5회 제한을 초과하는 요청은 실패 처리되어 유실되는 문제가 있었습니다. 이를 막기 위해 Redis의 List 자료형을 큐처럼 사용하면서, 제한을 넘어서는 요청을 저장하였습니다. 또, @Scheduled 어노테이션을 이용해 TaskScheduler가 5초마다 큐에 쌓인 요청을 꺼내 처리하도록 구현하였습니다.
이와 별개로 사용자가 많아졌을 때를 가정하여, 요청 처리량을 늘리기 위해 멀티 계정 구조를 적용하였습니다. 복수 개의 API 키를 환경변수로 받아, 각각 토큰 버킷을 따로 두면서 어느 API 키 사용량이 초과되었을 때 다른 API 키를 사용하도록 하였습니다.
@Async, WebClient (Netty)
API 사용을 위해 HTTP 요청을 보낼 때 어떻게 보낼지 정할 때도 선택할 것이 있었습니다.
RestTemplate
으로 HTTP 요청을 보내고, 요청 메서드에 @Async
를 붙이거나, WebClient
를 사용하거나 둘 중 하나를 택해야 했습니다.
RestTemplate
같은 경우 동기/블로킹 방식이지만 @Async
를 붙이게 되면 스프링에서 AOP를 이용해 요청 전송을 다른 스레드에서 진행하기 때문에 호출한 쪽은 바로 리턴 받습니다. (스레드풀의 크기만큼 동시 처리가 가능하고, 스레드 리소스 소비가 큰 편.)
WebClient
는 Reactor 패턴의 비동기/논블로킹 방식으로, 내부적으로 Netty의 event-loop 모델을 사용하기 때문에 요청을 보낸 후 응답이 도착할 때까지 스레드를 점유하지 않고 반납합니다. (소수의 스레드만으로도 수천~수만 건의 요청을 동시에 처리할 수 있어 스레드 리소스 소비가 효율적.)
비동기에 익숙하지 않았던 터라 완전히 이해하진 못했지만, 좋은 공부가 되었습니다.
토큰 재발급 시 멀티 디바이스 지원 문제
Walkit이란 산책로 안내 서비스를 개발 팀 프로젝트에서는 인증 인가를 담당하여 OAuth 2 소셜 로그인 기능 개발을 담당하였습니다.
문제 배경
서비스에선 인증을 위해 유효기한이 짧은 액세스 토큰과 유효기한이 긴 리프레시 토큰을 별도로 발급합니다. 이때 액세스 토큰이 유효하지 않으면, 액세스 토큰을 재발급할 수 있도록 개발하였습니다.
리프레시 토큰의 경우 Redis에 저장, 재발급 시 활용합니다.
그러나, 다른 장치나 브라우저에서 로그인할 경우 서버에 저장된 리프레시 토큰이 덮어써지는 문제가 발생하였습니다.
해결 방안
이와 같은 구조에서 재발급 기능이 작동하기 위해, 같은 회원에 대해서도 여러 개의 리프레시 토큰을 저장할 필요가 있었습니다.
이를 해결하기 위해 브라우저마다 랜덤한 UUID 값을 deviceId
로 생성한 뒤 LocalStorage에 저장해 두었습니다. OAuth 2 로그인 시 쿼리 파라미터 state
값으로 deviceId
를 포함하여, 인증 서버에서 클라이언트로 돌아올 때, deviceId
값을 key에 포함하여 Redis에 저장하도록 개선하였습니다.
구름톤에서 팀 프로젝트를 진행하면서 백엔드 멘토의 멘토링 과정도 있었습니다. 사실 위 문제는 개발 시작 전 우연히 회사에서 소셜 로그인 개발을 몇 달 담당해 보신 멘토님을 만나 얘기들은 것이기도 하였습니다. 액세스 토큰이 요청을 보낼 땐 유효했는데, 전송 지연으로 인해 만료되면 어떻게 할 것인가, 여러 디바이스에서 로그인할 때 어떻게 할 거냐 등 고려해야 할 부분을 알려주셨습니다. 당시에는 말씀을 들어도 정확히 이해하지 못했지만, 직접 토큰 재발급 기능을 구현하면서 '아, 이렇게 해야 하는구나' 하고 체감할 수 있었습니다.
'개발 > Spring' 카테고리의 다른 글
[Spring] 스프링 부트, 요청이 몰리면 어떻게 될까? (0) | 2025.04.09 |
---|