"배포했는데 왜 안 돼요?", "이 기능, 너무 불편해요!"
고객의 목소리는 서비스를 성장시키는 가장 중요한 자양분입니다. 특히 B2C 서비스를 운영한다면, 사용자의 피드백, 그중에서도 앱 스토어 리뷰에 담긴 생생한 목소리를 얼마나 빠르고 정확하게 듣느냐가 서비스의 품질과 고객 만족도를 좌우합니다.
저희 팀은 고객의 목소리를 빠르게 듣고 대응하기 위해 매일 발생하는 수많은 앱 리뷰를 별도의 시스템을 통해 수신하고 있었습니다. 하지만 기존 시스템에는 치명적인 문제가 있었습니다. 바로 리드타임(Lead Time)이었습니다.
문제점: 앱 리뷰를 수신하는 데 평균 2~3일이 소요되어, 고객이 불편을 겪는 시점과 우리가 문제를 인지하는 시점 사이에 큰 시간 차이가 발생했습니다. 이는 신속한 대응을 어렵게 만들었고, 이미 해결된 이슈에 대한 리뷰를 뒤늦게 받는 등 운영상의 피로도를 가중시켰습니다.
원인 분석: 기존 시스템은 외부 크롤링 플랫폼(예: App Follow)을 사용하여 Android와 iOS 양대 스토어의 리뷰를 수집하고 있었습니다.
문제는 바로 크롤링 주기였습니다. 크롤링 특성상 실시간 수집이 어렵고, 정해진 주기에 따라 정보를 수집하므로 지연이 발생할 수밖에 없었습니다. 외부 플랫폼을 사용했기 때문에 이 크롤링 주기를 우리가 직접 제어하기도 어려웠죠.
🤔 꼬리 질문: 외부 SaaS(Software as a Service)를 사용하는 것과 자체 시스템을 구축하는 것 사이에는 어떤 트레이드오프가 있을까요? 비용, 개발 속도, 유지보수, 기능 확장성 측면에서 각각의 장단점을 논의해 볼 수 있을까요?
"답답한 사람이 우물을 판다"는 속담처럼, 우리는 더 빠른 리뷰 수신을 위해 직접 시스템을 구축하기로 결정했습니다. 백엔드 개발자에게 크롤링보다는 API 활용이 훨씬 더 익숙하고 안정적인 방법이었기에, 각 앱 스토어에서 공식적으로 제공하는 API를 통해 리뷰 데이터를 직접 수신하는 방식으로 시스템을 새롭게 설계했습니다.
신규 시스템 설계:
이러한 설계를 통해 외부 플랫폼의 크롤링 주기에 의존하지 않고, 우리가 원하는 시점에 능동적으로 데이터를 가져와 리드타임을 획기적으로 단축하는 것을 목표로 했습니다.
각 스토어의 공식 API를 사용하려면, 우리가 정당한 권한을 가진 개발자임을 증명하는 인증 절차를 거쳐야 합니다.
I. Google Play Store (Android) 인증:
Google Play Store API에 접근하기 위해서는 OAuth 2.0 기반의 인증이 필요합니다. 다양한 인증 시나리오 중, 우리는 서버 간 통신에 적합한 서비스 계정(Service Account) 시나리오를 채택했습니다. 서비스 계정을 사용하면 사용자 개입 없이 서버 자체가 고유 ID로 인증을 수행할 수 있습니다.
인증 절차 요약:
구현 예시 (Kotlin): Google에서 제공하는 공식 클라이언트 라이브러리를 사용하면 이 과정을 비교적 쉽게 구현할 수 있습니다.
// Google Play Console API를 사용하기 위한 인증된 클라이언트를 생성하는 함수 예시
fun authenticateWithGoogle(appInfo: AppInfo): AndroidPublisher {
val credential = authorizeWithServiceAccount(appInfo) // 서비스 계정으로 인증
return AndroidPublisher.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential)
.setApplicationName(appInfo.packageName)
.build()
}
// 서비스 계정 키 파일을 사용하여 Credential 객체를 생성하는 함수 예시
private fun authorizeWithServiceAccount(appInfo: AppInfo): Credential {
// 클래스패스에서 키 파일 로드
val keyFileInputStream = this.javaClass.classLoader.getResourceAsStream(appInfo.keyFilePath)
?: throw IOException("Key file not found at: ${appInfo.keyFilePath}")
// GoogleCredential 빌더를 사용하여 인증 정보 구성
return GoogleCredential.Builder()
.setTransport(HTTP_TRANSPORT)
.setJsonFactory(JSON_FACTORY)
.setServiceAccountId(appInfo.serviceAccountEmail)
.setServiceAccountScopes(Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER)) // 필요한 권한 범위 설정
.setServiceAccountPrivateKeyFromP12File(keyFileInputStream) // P12 파일에서 개인키 로드
.build()
}
II. Apple App Store (iOS) 인증:
Apple App Store Connect API에 접근하기 위해서는 우리가 직접 생성한 JWT(JSON Web Token)를 통한 인증이 필요합니다.
인증 절차 요약:
Authorization
필드에 Bearer <JWT>
형태로 포함하여 App Store Connect API를 호출합니다.구현 예시 (Kotlin with Nimbus-JOSE-JWT library):
import com.nimbusds.jose.*
import com.nimbusds.jose.crypto.ECDSASigner
import com.nimbusds.jwt.*
import java.security.interfaces.ECPrivateKey
import java.util.Date
// JWT를 생성하고 서명하는 함수 예시
fun generateSignedAppleJWT(config: AppleApiConfig): String {
val now = Date()
val expirationTime = Date(now.time + 15 * 60 * 1000) // 15분 후 만료
// JWT 헤더 생성
val header = JWSHeader.Builder(JWSAlgorithm.ES256)
.keyID(config.keyId)
.type(JOSEObjectType.JWT)
.build()
// JWT 클레임(페이로드) 설정
val claimsSet = JWTClaimsSet.Builder()
.issuer(config.issuerId)
.issueTime(now)
.expirationTime(expirationTime)
.audience("appstoreconnect-v1")
.build()
val signedJWT = SignedJWT(header, claimsSet)
// Private Key로 서명
val privateKey: ECPrivateKey = loadPrivateKey(config.privateKeyPath) // 키 파일 로드 로직
val signer = ECDSASigner(privateKey)
signedJWT.sign(signer)
return signedJWT.serialize()
}
🤔 꼬리 질문: OAuth 2.0과 JWT 인증 방식은 어떤 근본적인 차이가 있을까요? 상태 저장(Stateful)과 무상태(Stateless) 관점에서 두 방식을 비교 설명해 볼 수 있을까요?
인증 절차를 완료했다면, 이제 각 스토어의 공식 API를 통해 리뷰 데이터를 자유롭게 조회할 수 있습니다.
https://developers.google.com/android-publisher/reply-to-reviews
https://developer.apple.com/documentation/appstoreconnectapi/customer_reviews
조회한 리뷰 데이터를 팀원들이 쉽게 확인할 수 있도록 사내 메신저인 Slack으로 전송하기로 했습니다. Slack API가 제공하는 풍부한 기능을 활용하면 단순히 텍스트를 보내는 것을 넘어, 가독성 높은 메시지를 만들 수 있습니다.
WebClient
와 같은 비동기 HTTP 클라이언트를 사용하여 Block Kit으로 만든 JSON 페이로드를 Incoming Webhook URL로 전송하면, 디자이너나 프론트엔드 개발자의 도움 없이도 백엔드 개발자 혼자서 멋진 디자인의 알림 메시지를 구현할 수 있습니다!
한 번 만들고 끝나는 시스템은 없습니다. 새로운 리뷰 수신 시스템을 안정적으로, 그리고 지속적으로 운영하려면 CI/CD(Continuous Integration/Continuous Deployment) 파이프라인 구축이 필요했습니다. 저희는 주로 AWS 서비스를 활용하여 다음과 같이 간단하면서도 효과적인 CI/CD 구조를 채택했습니다.
main
브랜치에 코드를 푸시(Push)합니다.files
섹션에서는 어떤 파일을 어디로 복사할지, hooks
섹션에서는 배포 각 단계(예: ApplicationStop
, BeforeInstall
, ApplicationStart
)에서 어떤 스크립트를 실행할지를 정의합니다. 이를 통해 배포 과정을 세밀하게 제어할 수 있습니다.🤔 꼬리 질문: 위 CI/CD 파이프라인에서 CodeDeploy를 사용했을 때의 장점은 무엇일까요? 만약 CodeDeploy를 사용하지 않고 GitHub Actions에서 직접 EC2에 SSH로 접속하여 배포 스크립트를 실행하는 방식과 비교한다면 어떤 차이가 있을까요? (예: 배포 이력 관리, 롤백, 무중단 배포 전략 등)
배포가 끝이 아닙니다. 내가 만든 시스템이 의도대로 잘 작동하는지 지속적으로 확인하는 모니터링이 중요합니다. 앱 리뷰 시스템의 핵심은 "리뷰가 누락 없이 잘 수신되는가?"이므로, 다음과 같은 모니터링 체계를 구축했습니다.
다행히 시스템이 안정화된 이후로는 에러 메시지를 자주 받지는 않지만, 이러한 모니터링 체계 덕분에 예기치 않은 문제에도 자신 있게 대응할 수 있게 되었습니다.
새로운 앱 리뷰 수신 시스템을 도입한 결과, 가장 큰 문제였던 리드타임을 획기적으로 단축할 수 있었습니다. 이전에는 2~3일씩 걸리던 리뷰 수신이 이제는 거의 실시간에 가깝게 이루어지면서, 고객의 목소리에 더욱 신속하고 유연하게 대응할 수 있게 되었습니다.
이렇게 수집된 소중한 리뷰들은 제품 기획, 마케팅 전략 수립, 운영 방식 개선, 경쟁사 분석 등 다양한 분야에서 중요한 데이터로 활발하게 활용되고 있습니다. 개발자로서 내가 만든 시스템이 서비스에 실질적인 가치를 더하고, 동료들의 업무를 돕는 모습을 보며 큰 성취감을 느낄 수 있었습니다.
어떻게 보면 사소할 수 있는 "불편함"에 익숙해지지 않고, 이를 기술적으로 개선해 나가는 과정은 개발자에게 큰 즐거움과 성장의 기회를 제공합니다. 이번 앱 리뷰 수신 시스템 개발기는 비록 거창한 기술적 도전은 아니었을지라도, 고객의 목소리라는 가장 중요한 가치에 더 가까이 다가가기 위한 의미 있는 여정이었습니다.
앞으로도 저희는 고객의 소중한 피드백에 귀 기울이고, 이를 신속하게 서비스에 반영하여 여러분의 경험이 더욱 즐거워질 수 있도록 열정을 다해 달려가겠습니다! 긴 글 읽어주셔서 감사합니다.