유저의 소리를 더 빠르게 듣는 법: 신규 앱 리뷰 수신 시스템 개발기

이동휘·2025년 6월 12일
0

매일매일 블로그

목록 보기
26/49

"배포했는데 왜 안 돼요?", "이 기능, 너무 불편해요!"

고객의 목소리는 서비스를 성장시키는 가장 중요한 자양분입니다. 특히 B2C 서비스를 운영한다면, 사용자의 피드백, 그중에서도 앱 스토어 리뷰에 담긴 생생한 목소리를 얼마나 빠르고 정확하게 듣느냐가 서비스의 품질과 고객 만족도를 좌우합니다.


1. 기존 시스템의 아쉬움: "리뷰 배송이 너무 느려요!"

저희 팀은 고객의 목소리를 빠르게 듣고 대응하기 위해 매일 발생하는 수많은 앱 리뷰를 별도의 시스템을 통해 수신하고 있었습니다. 하지만 기존 시스템에는 치명적인 문제가 있었습니다. 바로 리드타임(Lead Time)이었습니다.

  • 문제점: 앱 리뷰를 수신하는 데 평균 2~3일이 소요되어, 고객이 불편을 겪는 시점과 우리가 문제를 인지하는 시점 사이에 큰 시간 차이가 발생했습니다. 이는 신속한 대응을 어렵게 만들었고, 이미 해결된 이슈에 대한 리뷰를 뒤늦게 받는 등 운영상의 피로도를 가중시켰습니다.

  • 원인 분석: 기존 시스템은 외부 크롤링 플랫폼(예: App Follow)을 사용하여 Android와 iOS 양대 스토어의 리뷰를 수집하고 있었습니다.

    1. 고객이 앱 리뷰 작성 → 각 스토어에서 내용 검증 후 공식 등록
    2. 외부 플랫폼이 주기적으로 양쪽 스토어의 리뷰 데이터 크롤링
    3. 수집된 리뷰를 Slack으로 알림 전송

    문제는 바로 크롤링 주기였습니다. 크롤링 특성상 실시간 수집이 어렵고, 정해진 주기에 따라 정보를 수집하므로 지연이 발생할 수밖에 없었습니다. 외부 플랫폼을 사용했기 때문에 이 크롤링 주기를 우리가 직접 제어하기도 어려웠죠.

🤔 꼬리 질문: 외부 SaaS(Software as a Service)를 사용하는 것과 자체 시스템을 구축하는 것 사이에는 어떤 트레이드오프가 있을까요? 비용, 개발 속도, 유지보수, 기능 확장성 측면에서 각각의 장단점을 논의해 볼 수 있을까요?


2. 새로운 길을 찾아서: "API로 직접 데이터를 가져오자!"

"답답한 사람이 우물을 판다"는 속담처럼, 우리는 더 빠른 리뷰 수신을 위해 직접 시스템을 구축하기로 결정했습니다. 백엔드 개발자에게 크롤링보다는 API 활용이 훨씬 더 익숙하고 안정적인 방법이었기에, 각 앱 스토어에서 공식적으로 제공하는 API를 통해 리뷰 데이터를 직접 수신하는 방식으로 시스템을 새롭게 설계했습니다.

신규 시스템 설계:

  1. 고객이 앱 리뷰 작성 → 각 스토어에서 내용 검증 후 공식 등록
  2. 스토어에서 제공하는 공식 API를 직접 호출하여 앱 리뷰 데이터 수신
  3. 수신된 데이터를 가공하여 Slack으로 실시간 알림 전송

이러한 설계를 통해 외부 플랫폼의 크롤링 주기에 의존하지 않고, 우리가 원하는 시점에 능동적으로 데이터를 가져와 리드타임을 획기적으로 단축하는 것을 목표로 했습니다.


3. 앱 리뷰 수신 시스템 개발기: 4단계로 완성하기

STEP 1. 데이터 조회를 위한 인증 절차: "실례합니다, 누구시죠?"

각 스토어의 공식 API를 사용하려면, 우리가 정당한 권한을 가진 개발자임을 증명하는 인증 절차를 거쳐야 합니다.

I. Google Play Store (Android) 인증:

Google Play Store API에 접근하기 위해서는 OAuth 2.0 기반의 인증이 필요합니다. 다양한 인증 시나리오 중, 우리는 서버 간 통신에 적합한 서비스 계정(Service Account) 시나리오를 채택했습니다. 서비스 계정을 사용하면 사용자 개입 없이 서버 자체가 고유 ID로 인증을 수행할 수 있습니다.

  • 인증 절차 요약:

    1. Google Cloud Platform(GCP)에서 서비스 계정을 생성하고, 필요한 API(예: Google Play Android Developer API) 접근 권한을 부여합니다.
    2. 생성된 서비스 계정의 키 파일(JSON 또는 P12 형식)을 다운로드합니다. 이 파일에는 인증에 필요한 모든 정보(Private Key, Client ID 등)가 담겨 있습니다.
    3. 애플리케이션 코드에서 이 키 파일을 사용하여 서명된 JWT(JSON Web Token)를 생성하고, 이를 Google OAuth 2.0 서버로 보내 액세스 토큰(Access Token)을 발급받습니다.
    4. 발급받은 액세스 토큰을 HTTP 요청 헤더에 포함하여 Google Play Console API를 호출합니다.
    5. 액세스 토큰은 유효 기간이 있으므로, 만료 시 자동으로 재발급하는 로직이 필요합니다.
  • 구현 예시 (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)를 통한 인증이 필요합니다.

  • 인증 절차 요약:

    1. App Store Connect에서 API 키를 생성하고, Issuer ID, Key ID, 그리고 Private Key 파일(.p8)을 다운로드합니다.
    2. 애플리케이션 코드에서 이 정보들을 사용하여 JWT를 생성합니다. JWT의 헤더에는 알고리즘(ES256)과 Key ID를, 페이로드(Claims)에는 Issuer ID, 발급 시간, 만료 시간(최대 20분), 수신자("appstoreconnect-v1") 등을 포함해야 합니다.
    3. 생성된 JWT를 Private Key로 서명합니다.
    4. 서명된 JWT를 HTTP 요청 헤더의 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) 관점에서 두 방식을 비교 설명해 볼 수 있을까요?

STEP 2. 데이터 조회 후 가공 및 전송: "예쁘게 만들어서 슬랙으로 보내자!"

인증 절차를 완료했다면, 이제 각 스토어의 공식 API를 통해 리뷰 데이터를 자유롭게 조회할 수 있습니다.

  • Google Play (Android): https://developers.google.com/android-publisher/reply-to-reviews
  • App Store (iOS): https://developer.apple.com/documentation/appstoreconnectapi/customer_reviews

조회한 리뷰 데이터를 팀원들이 쉽게 확인할 수 있도록 사내 메신저인 Slack으로 전송하기로 했습니다. Slack API가 제공하는 풍부한 기능을 활용하면 단순히 텍스트를 보내는 것을 넘어, 가독성 높은 메시지를 만들 수 있습니다.

  • Slack Block Kit: JSON 기반의 UI 프레임워크로, 텍스트, 버튼, 이미지 등을 조합하여 일관되고 깔끔한 메시지 레이아웃을 구성할 수 있습니다.
  • Incoming Webhooks: 특정 Slack 채널에 매핑되는 고유한 URL 엔드포인트를 제공합니다. 이 엔드포인트로 HTTP POST 요청을 보내면 애플리케이션이 Slack 채널에 메시지를 게시할 수 있습니다.

WebClient와 같은 비동기 HTTP 클라이언트를 사용하여 Block Kit으로 만든 JSON 페이로드를 Incoming Webhook URL로 전송하면, 디자이너나 프론트엔드 개발자의 도움 없이도 백엔드 개발자 혼자서 멋진 디자인의 알림 메시지를 구현할 수 있습니다!

STEP 3. CI/CD 파이프라인 구축: "지속적인 운영을 위하여"

한 번 만들고 끝나는 시스템은 없습니다. 새로운 리뷰 수신 시스템을 안정적으로, 그리고 지속적으로 운영하려면 CI/CD(Continuous Integration/Continuous Deployment) 파이프라인 구축이 필요했습니다. 저희는 주로 AWS 서비스를 활용하여 다음과 같이 간단하면서도 효과적인 CI/CD 구조를 채택했습니다.

  • 주요 AWS 서비스:
    • EC2: 애플리케이션 서버를 실행할 가상 머신 공간.
    • S3: 배포할 애플리케이션 빌드 결과물(JAR 파일 등)을 저장하는 객체 스토리지.
    • CodeDeploy: S3에 저장된 빌드 결과물을 EC2 인스턴스에 자동으로 배포하고, 배포 과정을 관리하는 서비스.
  • 전체 배포 워크플로우:
  1. 개발자가 main 브랜치에 코드를 푸시(Push)합니다.
  2. GitHub Actions가 푸시 이벤트를 감지하고 워크플로우를 실행합니다.
    • 코드 체크아웃 → JDK 설정 → Gradle 빌드 수행
  3. 빌드가 완료되면, 생성된 JAR 파일과 배포 스크립트 등을 압축하여 S3 버킷에 업로드합니다.
  4. GitHub Actions가 AWS CodeDeploy에게 배포 명령을 내립니다.
  5. CodeDeploy는 AppSpec 파일에 정의된 명세에 따라 S3에 있는 배포 패키지를 가져와 EC2 인스턴스에 배포를 수행합니다. (예: 기존 애플리케이션 중지 → 새 파일 복사 → 새 애플리케이션 시작)
  • AppSpec 파일이란?
    • CodeDeploy가 배포 과정에서 수행할 작업을 정의하는 YAML 또는 JSON 형식의 스펙 문서입니다. files 섹션에서는 어떤 파일을 어디로 복사할지, hooks 섹션에서는 배포 각 단계(예: ApplicationStop, BeforeInstall, ApplicationStart)에서 어떤 스크립트를 실행할지를 정의합니다. 이를 통해 배포 과정을 세밀하게 제어할 수 있습니다.

🤔 꼬리 질문: 위 CI/CD 파이프라인에서 CodeDeploy를 사용했을 때의 장점은 무엇일까요? 만약 CodeDeploy를 사용하지 않고 GitHub Actions에서 직접 EC2에 SSH로 접속하여 배포 스크립트를 실행하는 방식과 비교한다면 어떤 차이가 있을까요? (예: 배포 이력 관리, 롤백, 무중단 배포 전략 등)

STEP 4. 배포 후 모니터링: "우리 시스템, 잘 지내고 있니?"

배포가 끝이 아닙니다. 내가 만든 시스템이 의도대로 잘 작동하는지 지속적으로 확인하는 모니터링이 중요합니다. 앱 리뷰 시스템의 핵심은 "리뷰가 누락 없이 잘 수신되는가?"이므로, 다음과 같은 모니터링 체계를 구축했습니다.

  • 정상 수신 알림: 리뷰 수집 작업이 정상적으로 끝날 때마다, "총 N개의 리뷰를 수신하여 전송 완료했습니다." 와 같은 요약 메시지를 Slack으로 받아 시스템이 살아있음을 확인합니다.
  • 실패 알림: 리뷰 수집 또는 전송 과정에서 오류가 발생하면, 즉시 에러 로그와 함께 실패 알림을 Slack으로 받아 신속하게 대응할 수 있도록 구성했습니다.

다행히 시스템이 안정화된 이후로는 에러 메시지를 자주 받지는 않지만, 이러한 모니터링 체계 덕분에 예기치 않은 문제에도 자신 있게 대응할 수 있게 되었습니다.


4. 개선 결과와 성과: 고객의 목소리에 더 가까이!

새로운 앱 리뷰 수신 시스템을 도입한 결과, 가장 큰 문제였던 리드타임을 획기적으로 단축할 수 있었습니다. 이전에는 2~3일씩 걸리던 리뷰 수신이 이제는 거의 실시간에 가깝게 이루어지면서, 고객의 목소리에 더욱 신속하고 유연하게 대응할 수 있게 되었습니다.

이렇게 수집된 소중한 리뷰들은 제품 기획, 마케팅 전략 수립, 운영 방식 개선, 경쟁사 분석 등 다양한 분야에서 중요한 데이터로 활발하게 활용되고 있습니다. 개발자로서 내가 만든 시스템이 서비스에 실질적인 가치를 더하고, 동료들의 업무를 돕는 모습을 보며 큰 성취감을 느낄 수 있었습니다.


마무리하며

어떻게 보면 사소할 수 있는 "불편함"에 익숙해지지 않고, 이를 기술적으로 개선해 나가는 과정은 개발자에게 큰 즐거움과 성장의 기회를 제공합니다. 이번 앱 리뷰 수신 시스템 개발기는 비록 거창한 기술적 도전은 아니었을지라도, 고객의 목소리라는 가장 중요한 가치에 더 가까이 다가가기 위한 의미 있는 여정이었습니다.

앞으로도 저희는 고객의 소중한 피드백에 귀 기울이고, 이를 신속하게 서비스에 반영하여 여러분의 경험이 더욱 즐거워질 수 있도록 열정을 다해 달려가겠습니다! 긴 글 읽어주셔서 감사합니다.

0개의 댓글