2024.10.07 - [Project] - [포텐데이 409-1pick] 올려올려 라디오 서비스 개발기 - 1
[포텐데이 409-1pick] 올려올려 라디오 서비스 개발기 - 1
우리가 만든 서비스https://upup-radio.site/ 올려올려 라디오언제 어디서나 DJ가 여러분에게 따뜻한 위로를 전해드립니다.upup-radio.site글을 쓰고 있는 현재, 우리 팀이 만든 서비스가 1Pick을 받았습니
dev-gallery.tistory.com
이전 포스팅에 이어서 글을 작성합니다.
우리 팀이 만든 서비스는 아래 링크를 통해 이용이 가능합니다.
올려올려 라디오
언제 어디서나 DJ가 여러분에게 따뜻한 위로를 전해드립니다.
upup-radio.site
API 요청 처리 제한기
아무래도 외부 API와 연동을 하면 걱정되는 부분이 요금 관련 문제가 있습니다.
NCP(Naver Cloud Platform)의 Clova Studio 요금 설명에 따르면 챗 모드에 사용되는 HCX-003 모델은 대락 1,000 토큰에 5원 정도가 청구됩니다.

저희 서비스에서 요청 메시지에 들어가는 입력 토큰(대략 200 토큰)과 최대 출력 토큰 (700토큰)을 합치면 1회 요청 당 평균 5원
씩 청구될 수 있습니다.
100명의 유저가 하루에 2번씩만 사용한다고 가정하면 하루 평균 1,000원
이며 한달 평균 30,000원
정도가 예상됩니다.
지원받은 크레딧이 넉넉하게 남았지만, 지원받지 않는 상황이라면 상당한 부담이 될 수 있으며 예기치않은 비용 청구를 받을 수 있습니다.
이 때문에 처리율 제한 장치는 반드시 필요한 상황입니다.
처리율 제한 장치의 위치
클라이언트 vs 서버
클라이언트에 둘 때
분산 환경에서 세션을 두면 필연적으로 정합성 문제가 발생하기 때문에 무상태(stateless)를 유지해야 합니다.
따라서 쿠키, JWT를 고려할 수 있습니다.
그러나, 일반적으로 클라이언트에 제한 장치를 두면 쿠키, JWT 등 위변조가 쉽기 때문에 적절하지 못합니다.
서버에 둘 때
서버에 제한 장치를 둔다면 위변조에 강하고, 처리율 제한을 통제하기가 비교적 수월합니다.
그러면 어디에 제한 장치를 둬야 할까요?

위의 구성도는 단일 서버 내에 Docker 컨테이너 환경입니다.
하나의 브릿지 네트워크 안에 Back-end
컨테이너와 Database
컨테이너가 있습니다.
처리율 제한 장치는 미들웨어로 두는 방법과 기존 네트워크에 놓는 방법이 있는데요, 아래 미들웨어에 둘 때와 같은 도커 네트워크(브릿지)에 둘 때 구성도를 보겠습니다.
미들웨어에 둘 때 (선택 X)

같은 도커 네트워크에 둘 때 (선택 O)

타임 스탬프나 IP 자체가 아닌 유저의 아이디를 제한하는 것을 염두에 뒀었기 때문에 기존 네트워크에 처리율 제한 장치를 추가하는 것을 선택했습니다.
(이후에 개선할 때는 미들웨어에 올리도록 개선의 여지를 두고나서 계속 진행해 보겠습니다.)
처리율 제한 장치를 만들 때 고려할 사항
적은 자원 소모
포텐데이 시작 후 웰컴 키트 2단계까지 지원했기 때문에 1)단일 서버(cpu-2 core, memory-8gb), 2)Global DNS, 3)Clova Studio를 사용하는데 최소 월 10만 크레딧 내외로 3개월 정도 사용해야 합니다.
즉, 별도 서버를 생성하지 않고 cpu 2 core
, 8gb memory
안에서 해결해야 하죠.
낮은 응답시간
유저가 편지를 작성하고 답변받기까지 소요되는 응답시간은 외부 API를 동기적으로 호출하기 때문에 입력 토큰에 따라 2,000~3,000ms 사이로 평균 2,500ms 정도 걸립니다.
답장을 받는 동안 유저는 로딩 화면에서 3초가량 머물게 되는데, 여기서 응답시간이 더 지연되면 사용자 경험을 크게 해칠 수 있습니다.
이전 포스팅에서 작성했듯이 추가 학습이 필요한 WebClient
클라이언트로 당장 바꿀 수 있는 것도 아닙니다.
분산형 처리율 제한
백엔드 서버 컨테이너가 여러 대일 경우 하나의 처리율 제한 장치를 공유해서 사용해야 합니다.
높은 결함 내성
처리율 제한 장치의 장애 발생이 서버 전체에 전파되면 안됩니다.
처리율 제한 알고리즘 고르기
아래에 5가지 정도의 처리율 제한 알고리즘과 의사결정을 위한 요약을 작성했습니다.
- 토큰 버킷(token bucket): 구현이 쉽고, 메모리 사용량이 적지만 버킷 크기와 공급률을 튜닝하기 까다롭다.
- 누출 버킷(leaky bucket): 메모리 사용량이 적고, 고정된 처리율 때문에 안정적으로 처리가 가능하지만 단시간에 몰린 요청을 처리하지 못하면 최신 요청들은 버려진다.
- 고정 윈도 카운터(fixed window counter): 이해하기 쉽고 메모리 효율이 좋지만, 윈도 경계 부근에서 일시적으로 트래픽이 몰리면 처리 한도보다 많은 요청을 처리하게 된다.
- 이동 윈도 로그(sliding window log): 정교한 메커니즘이지만, 거부된 요청의 타임스탬프도 로그로 남기기 때문에 메모리가 많이 필요하다.
- 이동 윈도 카운터(sliding window counter): 짧은 시간에 몰리는 트래픽에도 잘 대응할 수 있고, 메모리 효율도 좋지만 구현에 어느정도 시간이 필요하다.
위의 알고리즘 중 구현이 쉽고 메모리 사용량이 적은 토큰 버킷(token bucket)
알고리즘을 응용해 보기로 했습니다.
대신 나중에 튜닝이 까다로운 버킷 크기
와 공급률
은 운영단에서 적절하게 조절할 수 있도록 추가 구현을 하면 됩니다.
요청 처리 제한을 하는 방법
요청 처리를 제한할 때 제한하는 대상을 명확히 정의해야 합니다.
서비스에서 특정 엔드포인트 호출 횟수를 제한한다고 할 때, IP
를 제한할 것인지, 유저의 아이디
를 제한할 것인지 등을 정해야 합니다.
서비스가 유저를 구분하는 방식
우리 서비스는 로그인을 하지 않은 게스트 유저
와 로그인 유저
를 구분합니다.
실제 사용자의 디바이스 환경에 따라 다를 수 있지만 통상적으로 생각해 보겠습니다.
실제 사용자가 게스트로 이용하거나 로그인 후 이용할 때 같은 IP를 사용할 확률이 높습니다.
초기 서비스에서 게스트로 이용해보고 마음에 들면 OAuth2로 간편 로그인을 한 후 계속 이용할 수 있어야 합니다.
따라서 유저의 아이디
를 제한해야 합니다.
어떻게 제한할 것인가?
기획자님과 어떻게, 얼마나 제한할 것인지 논의를 한 결과, 2시간에 2회로 제한을 두기로 했습니다.
어디에 저장할 것인가?
분산 환경에서 유저마다 요청 횟수를 어디에 저장할지도 고려해야 합니다.
선택지는 두 가지 정도로 좁혀봤습니다.
- 서비스에서 데이터베이스로 MySQL을 사용하고 있기 때문에 Memory Engine을 사용할 수도 있습니다.
- Redis 같은 In-Memory DB를 사용할 수도 있습니다.
각기 장단점은 무엇이 있을까요?
MySQL Memory Engine
- 장점
- 1. 이미 가동중인 컨테이너를 재사용할 수 있어서 빠른 구축이 가능합니다.
- 2. 저장하려는 데이터의 형식이 정해져 있기 때문에 관계형에 적합합니다.
- 단점
- 1. 쓰기 작업은 InnoDB와 달리 Lock의 범위가 Table 입니다. 분산 환경에서 동시성이 좋지 않습니다.
- 2. 만료 시간이 됐을 때 삭제하는 작업(쓰기 작업)이 추가로 필요합니다.
Redis
- 장점
- 1. 싱글 스레드로 작업하기 때문에 분산 환경에서 높은 동시성이 보장됩니다.
- 2. 만료 시간이 되면 소멸하는 기능을 지원합니다.
- 단점
- 1. 별도의 컨테이너가 필요합니다.
종합적으로 판단해보면, MySQL Memory Engine을 사용하는 것도 하나의 방법이지만, 분산 환경에서 낮은 응답시간과 동시성을 고려해야 하기 때문에 적절하지 않습니다.
따라서 Redis를 이용해 저장합니다.
구현
구현 방법은 유저의 ID + 이용 횟수
를 Redis에 저장합니다. 이때, 최초 저장하는 것이라면 EXPIRE 를 2시간으로 설정합니다.
이로써 버킷에 시간당 고정적으로 2회를 제한할 수 있게 됩니다.

변수 설명
- USER_KEY_REMAINING: Redis에 저장할 Key 의 포맷입니다.
- REQUEST_LIMIT: 요청 제한 횟수입니다.
- EXPIRE_TIME: 만료 시간입니다. Redis에 저장된 값은 만료 시간이 지나면 자동으로 소멸됩니다.
요청이 거절되면 어떻게 처리해야 할까?

이제 처리율 제한 장치(RateLimitService
)를 사용할 때 요청 제한 횟수 초과 시 예외를 던지는 방식으로 처리를 제한합니다.
이 예외는 제가 커스텀한 예외로, 예외 발생 시 RestControllerAdvice
에 의해 핸들링됩니다.

Http 상태 코드는 429 Too Many Requests
(자세한 설명 보기)로 적절히 처리하면 됩니다.
개선할 점은 클라이언트에 요청 한도에 걸리지 않도록 X-Ratelimit-Remaining
, X-Ratelimit-Limit
, X-Ratelimit-Retry-After
응답 헤더와 함께 메시지를 보내면 되겠습니다.
다음 포스팅에선 Spring Security를 통해 OAuth2를 간단하게 구현한 방법에 대해 작성할 계획입니다.
긴 글 읽어주셔서 감사합니다.
참고
- 가상 면접 사례로 배우는 대규모 시스템 설계 기초(https://product.kyobobook.co.kr/detail/S000001033116)
- MySQL의 Memory Engine과 Redis 비교(https://velog.io/@yangsijun528/MySQL의-Memory-Engine과-Redis-비교)
- MySQL as Redis vs Redis? (https://dkomanov.medium.com/mysql-as-redis-vs-redis-74b788af9c6f)
'Project' 카테고리의 다른 글
[올려올려 라디오] 분석 서비스 개발 중 만난 데드락과 동시성 문제 해결기 (2) (0) | 2024.11.26 |
---|---|
[올려올려 라디오] 분석 서비스 개발 중 만난 데드락과 동시성 문제 해결기 (1) (0) | 2024.11.25 |
[올려올려 라디오] 올려올려 라디오 서비스 개발기 - 1 (4) | 2024.10.07 |
[예약 대기 시스템] 4. 컨테이너 환경에서 테스트하기 (Testcontainers) (4) | 2024.09.16 |
[예약 대기 시스템] 3. 프로젝트 설정 (어드민 시스템) (1) | 2024.09.13 |