Upgrade to Pro — share decks privately, control downloads, hide ads and more …

개발자가 성과를 증명하는 방법 | A/B테스트 기반 개발

개발자가 성과를 증명하는 방법 | A/B테스트 기반 개발

2024년 5월 22일 삼성전자 무선사업부 GDE 초대 강연에서 발표한 자료입니다. 과거 발표했던 자료의 템플릿을 변경하였습니다.

과거 자료 링크 👉 https://speakerdeck.com/maryang/b-teseuteu-giban-mobail-aeb-gaebal

개발을 수치로 환산하여 성과를 측정, 증명하는 것은 어렵습니다.
A/B 테스트는 내 코드가 서비스에 얼마만큼 기여했는지 데이터로 알려줍니다.
본 강의는 A/B 테스트를 구현하기위해 필요한 Observability, Feature Flag, Experiment 3가지 키워드를 개념부터 예시 코드까지 다룹니다.
그리고 데이터 기반으로 서비스 가치를 주도적으로 창출하는 개발자의 새로운 성장 방향을 소개합니다.

Seungmin 마량

May 22, 2024
Tweet

More Decks by Seungmin 마량

Other Decks in Programming

Transcript

  1. ੉थ޹ (੹) ߛ௼࢟۞٘ ೐ۿ౟ ݒפ੷ (੹) ٘ۄ݃ঙஹಌפ উ٘۽੉٘ ѐߊ੗ (੹)

    Pin the Cloud ؀಴ (അ) ীয೐ۨ޷ই ەझ ೐ۿ౟ ݒפ੷ (അ) GDE Android Korea
  2. ࢲ࠺झ ѐߊ਷… 규모 있는 서비스는 무엇이 장점이고 개선해야 하는지 알기

    어렵습니다. 그래서 데이터를 보고 인사이트를 얻어 기획 정확도를 높여야 합니다.
  3. ࢲ࠺झ ѐߊ਷… 규모 있는 서비스는 무엇이 장점이고 개선해야 하는지 알기

    어렵습니다. 그래서 데이터를 보고 인사이트를 얻어 기획 정확도를 높여야 합니다. 규모 있는 서비스는 만든 기능이 사용자가 원하는 것인지 알기 어렵습니다. 그래서 실험으로 사용자 행동을 측정하고 확률이 높은 결정을 해야 합니다.
  4. ࢲ࠺झ ѐߊ਷… 규모 있는 서비스는 무엇이 장점이고 개선해야 하는지 알기

    어렵습니다. 그래서 데이터를 보고 인사이트를 얻어 기획 정확도를 높여야 합니다. 규모 있는 서비스는 만든 기능이 사용자가 원하는 것인지 알기 어렵습니다. 그래서 실험으로 사용자 행동을 측정하고 확률이 높은 결정을 해야 합니다. 규모 있는 조직은 아무리 대표가 직관이 좋아도 모든 것을 결정할 수 없습니다. 그래서 팀이 스스로 정확한 의사결정을 할 수 있도록 실험해야 합니다.
  5. ѐߊ੗ח… 언어는 발전하고 각종 도구가 나오면서 개발은 나날이 쉬워지고 있습니다.

    기술을 잘 다루는 것만으로 성장하는 시대는 지나가고 있습니다. 기술을 수단으로써 서비스를 만드는 개발자로 성장하여야 합니다. 서비스에서 기회를 찾고 스스로 성과를 증명해야 합니다
  6. ੉ ߊ಴ח… 1. 데이터 기반으로 앱 서비스를 만드는 과정을 이해합니다.

    2. 이벤트, 피쳐플레그, 실험 3가지 키워드를 이해합니다. 3. 데이터 기반으로 서비스 가치를 창출하는 새로운 성장 방향을 이해합니다. 실험 기반으로 앱 서비스를 개선하는 과정을 개발자 관점에서 정리하였습니다
  7. ࢎਊ੗ ೯ز੉ۆ ച 면 ੉ز ௿ܼ ੤ߑޙ ਖ਼ઓ  결

    ઁ ஶబஎࢤࢿ١ ࢎਊ੗оࢲ࠺झী 서 ೞחݽٚ೯ز
  8. ചݶ ೯ز ఋ੉߁ য٣ীࢲ ೯زೞ৓חо যڃ ೯زਸ ೞ৓חо ঱ઁ ۽Ӧ

    ೡ Ѫੋо ੉ܴ ചݶ, ೯ز, ఋ੉߁ਸ ೞա੄ ੉ܴਵ۽ ಴അ ࣘࢿ э਷ ೯ز੉যب ׮ܲ ࢎਊ੗/੉߮౟ ࢚ടਸ ಴അ ੉߮౟ ҳࢿ
  9. ੉߮౟ ੿੄ ৘द No, ചݶ ࢎਊ੗ ೯زҗ ఋ੉߁ ੉ܴ ࣘࢿ੉ܴ

    / ч 1 ੗࢑ ചݶ ૓ੑ enter__assets_home 2 - ੗࢑ زӝച ࢿҕ success__sync_assets 3 ஠٘୶ୌ ࢚ಿ ௿ܼ click__product_card ੉ܴ: id / ч: ࢚ಿ ই੉٣ ч ৘द: 3425, 1829 ੉ܴ: company / ч: ӝҙ ੉ܴ ч ৘द: नೠ஠٘, Ҵ޹஠٘ 4 ݽٚ ചݶ জ द੘ ನӒۄ਍٘ ചݶ੉ 0ѐীࢲ 1ѐо غח द੼ open_app ੉ܴ: os ч ৘द: android, ios, web
  10. binding.contactButton.setOnClickListener { view -> analyticsManager.logEvent(EventDefinitions.clickContactHouseDetail()) startActivity( Intent(Intent.ACTION_DIAL, Uri.parse("tel:${houseLiveData.value.contact}")) ) }

    fun logEvent(event: EventDefinition) { firebaseAnalytics.logEvent(event.eventName, Bundle()) amplitude.logEvent(event.eventName) } 이벤트 코드 심기
  11. binding.contactButton.setOnClickListener { view -> analyticsManager.logEvent(EventDefinitions.clickContactHouseDetail()) startActivity( Intent(Intent.ACTION_DIAL, Uri.parse("tel:${houseLiveData.value.contact}")) ) }

    fun logEvent(event: EventDefinition) { firebaseAnalytics.logEvent(event.eventName, Bundle()) amplitude.logEvent(event.eventName) } ߡౡ ௿ܼ द ੉߮౟ ੹࣠ 이벤트 코드 심기
  12. binding.contactButton.setOnClickListener { view -> analyticsManager.logEvent(EventDefinitions.clickContactHouseDetail()) startActivity( Intent(Intent.ACTION_DIAL, Uri.parse("tel:${houseLiveData.value.contact}")) ) }

    fun logEvent(event: EventDefinition) { firebaseAnalytics.logEvent(event.eventName, Bundle()) amplitude.logEvent(event.eventName) } Firebase, Amplitude ١ ؘ੉ఠ ೒ۖಬী ੉߮౟ ੹࣠ 이벤트 코드 심기
  13. class BaseActivity : Activity() { protected open var enterEvent: EventDefinition?

    = null override fun onResume() { super.onResume() if (enterEvent != null) { analyticsManager.logEvent(enterEvent!!) } } } class UploadTypeActivity : BaseActivity() { override var enterEvent: EventDefinition? = EnterEventDefinitions.uploadType() } class HouseListActivity : BaseActivity() { override var enterEvent: EventDefinition? = EnterEventDefinitions.houseList() } Enter 이벤트 코드
  14. class BaseActivity : Activity() { protected open var enterEvent: EventDefinition?

    = null override fun onResume() { super.onResume() if (enterEvent != null) { analyticsManager.logEvent(enterEvent!!) } } } class UploadTypeActivity : BaseActivity() { override var enterEvent: EventDefinition? = EnterEventDefinitions.uploadType() } class HouseListActivity : BaseActivity() { override var enterEvent: EventDefinition? = EnterEventDefinitions.houseList() } ചݶ૓ੑ ࢤݺ઱ӝী ੉߮౟ ௏٘ ࢗੑ Enter 이벤트 코드
  15. class BaseActivity : Activity() { protected open var enterEvent: EventDefinition?

    = null override fun onResume() { super.onResume() if (enterEvent != null) { analyticsManager.logEvent(enterEvent!!) } } } class UploadTypeActivity : BaseActivity() { override var enterEvent: EventDefinition? = EnterEventDefinitions.uploadType() } class HouseListActivity : BaseActivity() { override var enterEvent: EventDefinition? = EnterEventDefinitions.houseList() } ചݶ݃׮ ੉ܴ ੿੄ Enter 이벤트 코드
  16. ࢲ࠺झ ઱ਃ ૑಴ܳ ؀಴ೞח ੉߮౟ܳ ੿೧ঠ ೤פ׮ Active User(AU) ӝળ

    ੉߮౟ ࢲ࠺झ ೨ब ੉߮౟ ࢲ࠺झо ઺ਃೞѱ ࢤпೞח ೨ब ࢎਊ੗ ೯زਸ ੿੄೧ঠ ೤פ׮. ‘জਸ ࢎਊೞҊ ੓ח ਬ੷੉׮’ܳ ౸ױೡ ࣻ ੓ח ೯زਸ ੿੄೧ঠ ೤פ׮.
  17. Active User(AU) ӝળ ੉߮౟ ৘द झ೒ېद ചݶਸ ૓ੑೞݶ AU۽ ನೣೠ׮

    ۽Ӓੋ റ ക ചݶী ૓ੑೞݶ AU۽ ನೣೠ׮ যڃ ചݶ੉ٚ ನӒۄ਍٘۽ ৢۄয়ݶ AU۽ ನೣೠ׮
  18. ࢲ࠺झ ೨ब ੉߮౟ ৘द ழݠझ ࢲ࠺झ੉޲۽ Ѿઁ ೯زਸ ઱ਃ ੉߮౟۽

    ஏ੿ೠ׮ SNS੉޲۽ ஶబஎ ࢤࢿ ೯زਸ ઱ਃ ੉߮౟۽ ஏ੿ೠ׮ ࠗز࢑ ઺ѐ ࢲ࠺झ੉޲۽ ࠗز࢑ োۅ ೯زਸ ઱ਃ ੉߮౟۽ ஏ੿ೠ׮
  19. AU ࠙ࢳ ੉߮౟ߊࢤࣻܳࠁח"6࠙ 석 DAU 10݅ݺ ઺, 3݅ݺ੉ Ѥъ ചݶী

    ٜযৡ׮ Ѥъ ചݶী ٜযয়ח 3݅ݺ ઺ 2݅ݺ੉ ੗࢑োز੉ غয੓׮ Ѥъ ചݶী ٜযয়ח 3݅ݺ ઺ 1ୌݺ੉ ߡౡਸ ׂ۞ ੗࢑োزਸ दبೠ׮ enter property click
  20. AU ࠙ࢳ ੉߮౟ߊࢤࣻܳࠁח"6࠙ 석 DAU 10݅ݺ ઺, 3݅ݺ੉ Ѥъ ചݶী

    ٜযৡ׮ Ѥъ ചݶী ٜযয়ח 3݅ݺ ઺ 2݅ݺ੉ ੗࢑োز੉ غয੓׮ Ѥъ ചݶী ٜযয়ח 3݅ݺ ઺ 1ୌݺ੉ ߡౡਸ ׂ۞ ੗࢑োزਸ दبೠ׮ enter property click Ѥъ ӝמ਷ ਬ੷੄ 30%о ࠁҊ Ӓ઺ 66%о োزغয ੓׮ োزغয੓૑ ঋ਷ ࢎਊ੗ ઺ 10%о োزਸ दبೠ׮
  21. ܻబ࣌ ࠙ࢳ ౠ੿೯ز੉റੌ੿दрٍجইয়ח ܻబ࣌࠙ 석 ੗࢑ োزਸ दب೮؍ ࢎۈ਷ दبೞ૑

    ঋও؍ ࢎۈࠁ׮ (Click) 7ੌ ղ জਸ ੤ߑޙೞח ࠺ਯ੉ (open_app) 10% ֫׮
  22. ౠ੿೯ز੉റੌ੿दрٍجইয়ח ܻబ࣌࠙ 석 ੗࢑ োزਸ दب೮؍ ࢎۈ਷ दبೞ૑ ঋও؍ ࢎۈࠁ׮

    (Click) 7ੌ ղ জਸ ੤ߑޙೞח ࠺ਯ੉ (open_app) 10% ֫׮ ੗࢑ োزਸ औѱ दبೡ ࣻ ੓ب۾ ѐࢶ೧ঠѷ׮ ܻబ࣌ ࠙ࢳ
  23. ௏ഐ౟ ࠙ࢳ ഥਗоੑਸ ਤ೧ Ѣ୛ঠೞח 3ѐ੄ ചݶ ઺ ࠄੋੋૐਸ ೞח

    2ߣ૩ ചݶীࢲ ੉ఎ੉ ݆׮ ױ҅߹۽੉ఎܫਸࠁח௏ഐ౟࠙ 석
  24. ࠄੋੋૐਸ औѱ ೡ ࣻ ੓ب۾ ѐࢶ೧ঠѷ׮ ഥਗоੑਸ ਤ೧ Ѣ୛ঠೞח 3ѐ੄

    ചݶ ઺ ࠄੋੋૐਸ ೞח 2ߣ૩ ചݶীࢲ ੉ఎ੉ ݆׮ ױ҅߹۽੉ఎܫਸࠁח௏ഐ౟࠙ 석 ௏ഐ౟ ࠙ࢳ
  25. ؘ੉ఠ ࠺Ү জ ੹୓ AU ઺ ੗࢑ AUח 80%, о҅ࠗ

    AUח 60%, ੗زର AUח 10%੉׮ ֢റח AUо ծ૑ ঋ૑݅, ܻబ࣌੉ ݽٚ ӝמ ઺ ઁੌ ծ׮ ӝמՙܻ઱ਃ૑಴࠺Ү
  26. ؘ੉ఠ ࠺Ү জ ੹୓ AU ઺ ੗࢑ AUח 80%, о҅ࠗ

    AUח 60%, ੗زର AUח 10%੉׮ ֢റח AUо ծ૑ ঋ૑݅, ܻబ࣌੉ ݽٚ ӝמ ઺ ઁੌ ծ׮ ӝמՙܻ઱ਃ૑಴࠺Ү ੗زର৬ ֢റח ࢲ࠺झ੄ ઱ਃ ӝמ੉ ইפ׮
  27. private fun setContactButton() { if (RemoteConfig.getBoolean(ConfigVariable.MoveContactButtonBottomToFloating)) { binding.contactButton.visibility = View.GONE

    binding.contactButtonFab.visibility = View.VISIBLE } else { binding.contactButton.visibility = View.VISIBLE binding.contactButtonFab.visibility = View.GONE } } Feature Flag로 기능 키고 끄기
  28. private fun setContactButton() { if (RemoteConfig.getBoolean(ConfigVariable.MoveContactButtonBottomToFloating)) { binding.contactButton.visibility = View.GONE

    binding.contactButtonFab.visibility = View.VISIBLE } else { binding.contactButton.visibility = View.VISIBLE binding.contactButtonFab.visibility = View.GONE } } Feature Flag로 기능 키고 끄기
  29. private fun setContactButton() { if (RemoteConfig.getBoolean(ConfigVariable.MoveContactButtonBottomToFloating)) { binding.contactButton.visibility = View.GONE

    binding.contactButtonFab.visibility = View.VISIBLE } else { binding.contactButton.visibility = View.VISIBLE binding.contactButtonFab.visibility = View.GONE } } Feature Flag에 따라 기능 분기
  30. private fun setContactButton() { if (RemoteConfig.getBoolean(ConfigVariable.MoveContactButtonBottomToFloating)) { binding.contactButton.visibility = View.GONE

    binding.contactButtonFab.visibility = View.VISIBLE } else { binding.contactButton.visibility = View.VISIBLE binding.contactButtonFab.visibility = View.GONE } } Feature Flag에 따라 기능 분기
  31. binding.contactButton.setOnClickListener { view -> analyticsManager.logEvent(EventDefinitions.clickContactHouseDetail()) startActivity( Intent(Intent.ACTION_DIAL, Uri.parse("tel:${contact}")) ) }

    binding.contactButtonFab.setOnClickListener { view -> analyticsManager.logEvent(EventDefinitions.clickContactHouseDetail()) startActivity( Intent(Intent.ACTION_DIAL, Uri.parse("tel:${contact}")) ) } Feature Flag에 따라 이벤트 개발
  32. binding.contactButton.setOnClickListener { view -> analyticsManager.logEvent(EventDefinitions.clickContactHouseDetail()) startActivity( Intent(Intent.ACTION_DIAL, Uri.parse("tel:${contact}")) ) }

    binding.contactButtonFab.setOnClickListener { view -> analyticsManager.logEvent(EventDefinitions.clickContactHouseDetail()) startActivity( Intent(Intent.ACTION_DIAL, Uri.parse("tel:${contact}")) ) } 실험으로 갈라지는 대조군, 실험군 기능에서 같은 이벤트를 전송 Feature Flag에 따라 이벤트 개발
  33. प೷ оࢸҗ Metric ৘द оࢸ ಹद੄ о҅ࠗ ৘࢑ࢸ੿ ޙҳܳ ߸҃ೞݶ,

    ৘࢑ࢸ੿ ৮ܐࣻо ֫ই૕ Ѫ੉׮. Primary Metric ৘࢑ࢸ੿ ৮ܐ ࣻ Second Metric ৘࢑ࢸ੿ ചݶ ߑޙ ࣻ Primary Metric ݽٚ ࢚ಿ ௿ܼ ࣻ ೤ Second Metric ୶ୌచ ߑޙ ࣻ ஠٘, ࠁ೷, ؀୹ ١ п ࢚ಿٜ੄ ௿ܼ ࣻ оࢸ ୶ୌచ ࢚ױ ߓցܳ ࢏ઁೞݶ ࢚ಿ ࣂ࣌੉ ਤ۽ ৢۄ৬, ࢚ಿ ௿ܼࣻо ֫ই૕ Ѫ੉׮.
  34. ӝഥ ߊҷ प೷ ࢸ҅ ѐߊ ؘ੉ఠ ࠙ࢳ ࢿҗ ب୹ ੋࢎ੉౟

    ೟ण ׮द ӝഥ ߊҷ ࢲ࠺झ ѐߊ੗੄ ࢎ੉௿
  35. ӝഥ ߊҷ प೷ ࢸ҅ ѐߊ ؘ੉ఠ ࠙ࢳ ࢿҗ ب୹ ੋࢎ੉౟

    ೟ण ׮द ӝഥ ߊҷ ࢲ࠺झী যڃ ߸҃ਸ ઱ݶ ௾ ࢿҗо զө? оࢸ Ҋউ ࢲ࠺झ ѐߊ੗੄ ࢎ੉௿
  36. ӝഥ ߊҷ प೷ ࢸ҅ ѐߊ ؘ੉ఠ ࠙ࢳ ࢿҗ ب୹ ੋࢎ੉౟

    ೟ण ׮द ӝഥ ߊҷ ࢲ࠺झ ѐߊ੗੄ ࢎ੉௿ оࢸਸ ࣻ஖ചػ ࢿҗ۽ ੿ഛ൤ Ѩૐ
  37. ӝഥ ߊҷ प೷ ࢸ҅ ѐߊ ؘ੉ఠ ࠙ࢳ ࢿҗ ب୹ ੋࢎ੉౟

    ೟ण ׮द ӝഥ ߊҷ ೟णೠ ੋࢎ੉౟۽ ؊ ੿ഛೠ оࢸ Ҋউ ࢲ࠺झ ѐߊ੗੄ ࢎ੉௿