[친구하자] Agora Cloud Recording과 Firebase Storage로 음성 통화 녹음 구현하기
Agora Cloud Recording을 통한 통화 녹음과 이를 Firebase Storage에 저장하는 기능을 구현하면서 경험한 것과 느낀 점을 정리해보았습니다.
‘친구하자’를 개발하며 통화 녹음 기능을 구현해야 했다. AI 개발과 통화 분석 등 추후 기능들을 위해 통화를 녹음하는 것을 전제로 구현을 시작했다.
처음에는 단순할 줄 알았는데, 생각보다 에러도 많이 나고 시행착오가 많아서 MVP에서 제외하고 개발했다. 이 글에서는 어떤 문제들을 겪었고, 어떻게 해결했는지를 정리하려고 한다.
목차
1. 기술 스택 선택
1.1. 왜 Agora Cloud Recording을 선택했나?
처음에 WebRTC 서비스를 선택하기에 앞서 Recording 구현을 고려했다. WebRTC 중에는 recording을 지원하는 서비스가 있고, 아닌 서비스가 있었다. 그중 Agora는 Recording 지원이 가장 다양했고, 가격도 저렴했다.
Recording을 저장하는 것에도 많은 선택지가 있었다:
- Agora 백업 저장소
- 제3자 스토리지 설정이 실패하면 백업 서버에 24시간 동안만 임시 저장
- 운영용이 아니므로 반드시 외부 저장소 연동 필요
- AWS S3 직접 연동
- 가장 일반적이고 안정적인 방법
- Firebase Storage (GCS 기반)
- 이미 프로젝트에서 사용 중 (프로필 이미지 등)
고민 끝에 Firebase Storage를 선택했다:
- 이미 프로젝트에서 사용 중이어서 익숙함
- GCS(Google Cloud Storage) 기반이라 Agora와 호환성이 좋음
- Firebase의 다른 기능들과도 잘 통합됨
- AWS S3와 비용이 비슷한 수준 (월 1,000분 통화 기준 약 80원 차이)
1.2. Individual Recording Mode 선택
Agora Cloud Recording에는 여러 모드가 있다:
- Composite Mode: 모든 사람의 음성을 하나로 합침
- Individual Mode: 각 사용자별로 별도 녹음
나는 Individual Mode를 선택했다. 추후 각 사용자의 음성을 따로 분석할 수도 있고, 더 유연하기 때문이다.
2. 구현 과정에서 만난 문제들
2.1. 비동기 처리 설정 문제
녹음 시작/중지는 시간이 걸리는 작업이기 때문에 비동기로 처리해야 했다. 그런데 Spring에서 이런 경고가 떴다:
More than one TaskExecutor bean found within the context,
and none is named 'taskExecutor'
문제:
@Async만 쓰면 Spring이 SimpleAsyncTaskExecutor를 사용한다. 이는 매번 새 스레드를 생성하기 때문에 매우 비효율적이다.
내 경우, 애플리케이션 컨텍스트에 이미 여러 개의 TaskExecutor 빈이 존재했는데 (recordingTaskExecutor, matchingTaskExecutor 등), 어느 것도 taskExecutor라는 이름을 가지고 있지 않았다. Executor를 지정하지 않고 @Async만 작성하니 Spring에서 경고를 보냈다.
해결:
용도별로 Executor를 분리하고, 기본 taskExecutor를 명시적으로 지정했다:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Bean("taskExecutor")
@Primary
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setThreadNamePrefix("Async-");
executor.initialize();
return executor;
}
@Bean("recordingTaskExecutor")
public Executor recordingTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("Recording-");
executor.initialize();
return executor;
}
}
이렇게 하면 스레드 풀을 재사용해서 성능이 훨씬 좋아진다:
- 스레드 생성 비용: ~1-2ms → ~0.01ms
- 리소스 제한: 무제한 → 최대 5개로 제어
2.2. Firebase Storage 저장 실패
드디어 녹음이 시작되었다! 로그에도 성공 메시지가 떴다. 그런데 Firebase Storage를 확인해보니… 파일이 없었다. 😱
문제의 핵심: uploadingStatus=backuped
uploadingStatus에는 두 가지 값이 있다:
uploaded: 제3자 스토리지(Firebase)에 업로드 성공backuped: Agora 자체 백업 서버에 저장 (24시간 후 삭제)
backuped가 나왔다는 건 Firebase에 업로드가 실패했다는 의미였다.
원인: HMAC 키가 없었다!
Agora가 GCS(Google Cloud Storage)에 파일을 올리려면 HMAC 인증 키가 필요하다. Firebase Console → Storage → Settings → Interoperability를 확인해보니 아무것도 없었다.
해결 과정:
Google Cloud Console 접속
Cloud Console → Storage → Settings → InteroperabilityHMAC 키 생성
Service account HMAC 섹션 → Create a key for a service account → firebase-adminsdk-xxxxx@프로젝트명.iam.gserviceaccount.com 선택 → CREATE KEYAccess Key와 Secret 복사
Access Key: GOOG1E...로 시작하는 긴 문자열 Secret: 한 번만 보여주니 즉시 복사!application.yml 설정
app: agora: recording-region: 0 # GCS Multi-region US recording-storage-vendor: "6" # 6 = GCS recording-storage-bucket: 프로젝트명.appspot.com recording-storage-access-key: ${AGORA_STORAGE_ACCESS_KEY} recording-storage-secret-key: ${AGORA_STORAGE_SECRET_KEY}
이렇게 설정하고 다시 녹음을 시작했더니… 드디어 성공! 🎉
3. 최종 구조
모든 삽질을 마치고 완성된 구조는 다음과 같다:

3.1. 주요 특징
- Individual Recording Mode: 각 사용자별로 별도 녹음
- Audio Only: 비용 절감 (비디오 없음)
- 자동 업로드: Agora → Firebase Storage 직접 저장
- 파일 구조화: 날짜/Call ID별 폴더 정리
- 비동기 처리: 녹음 시작/중지가 메인 스레드를 블로킹하지 않음
4. 배운 점
4.1. 공식 문서를 꼼꼼히 읽자
Agora 공식 문서에 Individual Mode와 Composite Mode의 차이가 명확하게 나와 있었다. 처음부터 제대로 읽었으면 불필요한 시행착오를 줄일 수 있었을 것이다.
4.2. 클라우드 서비스는 인증이 핵심
Firebase Storage에 파일이 안 올라가는 이유가 HMAC 키 때문이었다. 클라우드 간 연동에서는 항상 인증 설정을 먼저 확인해야 한다.
4.3. 비동기 처리는 제대로 설정하자
@Async만 쓰고 넘어가면 안 된다. 제대로 된 ThreadPoolTaskExecutor를 설정해야 운영에서 안정적이다.
5. 운영 지표
현재 운영 중인 시스템의 주요 지표:
- 녹음 시작 시간: 평균 2-3초
- 파일 업로드: Agora가 자동 처리 (15초마다)
- DB 저장: 비동기 처리로 메인 로직에 영향 없음
- 스레드 풀: 최대 5개 스레드로 제한되어 안정적
비용 최적화:
- Audio Only로 비디오 대비 비용 1/5 수준
- maxIdleTime 설정으로 30초 무음 시 자동 종료
- streamMode: standard (Agora 권장)
6. 마치며
처음엔 “그냥 녹음만 하면 되는 거 아니야?”라고 쉽게 생각했는데, 실제로는 정말 많은 함정이 있었다:
- Individual vs Composite Mode 이해
- HMAC 키 설정
- 비동기 처리 구성
- 에러 핸들링
하나하나 해결하면서 많이 배웠다. 특히 클라우드 서비스 간 연동에서 인증과 권한 설정이 얼마나 중요한지 깨달았다.
이 글이 Agora Cloud Recording과 Firebase Storage를 연동하려는 누군가에게 도움이 되었으면 좋겠다. 😅