KARTE for App SDKのOSS化によるDeveloper Experience向上への取り組み

こんにちは、エンジニアの@tanakoです。

PLAIDでは、CX(顧客体験)プラットフォームKARTEの機能を iOS / Androidのネイティブアプリから活用できる KARTE for App のSDK(以降 SDK)をAppチームで開発しています。

さて、Appチームは去る4/7にSDKをOSS化し、ソースコードをGithub上で公開しました 🎉

またOSS化に伴い、SDKをv2(以降 v2)にメジャーアップデートしました。今回は、SDKをOSS化して解決したかった課題や、v2メジャーアップデートの概要、チームとしての取り組みをいくつか紹介します。(この記事は社内勉強会で@wasnotが発表した内容を基に、@tanakoが加筆修正したものです。)

KARTE for App SDKについて

前提知識となるSDKについて簡単に説明します。SDKは、イベント送信APIやJavaScriptモジュールを読み込むコンポーネントを内包しており、管理画面で記述されたHTML/CSS/JavaScriptをSDKが管理するWebView(iOSではWKWebView)に読み込むことでアプリ内メッセージを表示できます。

OSS化の背景

KARTE SDKは他の一般的な計測SDKと異なり、活用の仕方によっては積極的にアプリのUIに介入できます。また、OSS化以前のv1 SDK(以降v1)はほぼクローズドソースで提供されていたため、自社アプリへの影響を心配する、次のフィードバックがいくつかのクライアントからありました。

  • コードの品質を直接確認できないため導入に不安がある
  • SDKアップデート時の差分をコードレベルで把握できず影響が読めない

これらの不安に対して我々がSDKの品質に問題ないと言ったところで、それを明確に証明する材料はなく説得力も持ちません。

このようなクライアントに安心して利用してもらうためにも、ソースコードの公開が必要と判断しました。

OSS化によって提供できる価値

OSS化するにあたり、利用者に提供できる価値を次のように明文化しました。

安心感の向上

  • SDKの品質に問題がないか確認できる安心感
  • 内部挙動を正確に把握できる安心感

信頼性の向上

  • 第三者が開発に参加することによる信頼性の向上
  • 多数の開発者の目に触れる(問題のあるコードは速やかに報告され修正されやすい)

透明性の向上

  • 開発プロセスの見える化
  • コードの変更経緯が追える

これらの価値提供でクライアントの不安は払拭できる一方で、単純にOSS化するだけでは、次に挙げる価値を直接提供することはできません。

OSS化だけでは提供できない価値

  • 機能性
  • 拡張性
  • 保守性

OSS化にあたり事前に決めたこと

えいやでソースコードを公開できれば楽ですが、当然OSS化後も開発は続きます。効果的、効率的にOSSを運用するにあたり次の点を決める必要がありました。それぞれどのような観点で決定したかを紹介します。

  • ライセンスの選定
  • 開発・リリースフローの検討
  • バージョニングポリシーの変更

ライセンスの選定

OSSとしてソースコードを公開するにあたり、コードのライセンスについて検討・選定しました。MIT、Apache 2.0、BSDや独自ライセンスなどが候補に挙がりましたが、社内有識者やリーガルチームと相談・議論し、条項の読み合わせを行なった上で、 次の理由でApache 2.0 Licenseを採用しました。

  • OSIの承認ライセンス
  • 利用率の高さ(非コピーレフトだとMITの次点)
  • 特許紛争に関する特記事項の存在

特に特許紛争に関する特記事項が記載されていることはクライアントに安心して利用して頂く上で重要なポイントです。

ライセンスの検討にあたっては、「OSSライセンスの教科書」を参考にしました。「OSSマトリョーシカ問題(OSS内で利用するOSSが競合するライセンスを持っている状態)」という言葉を知り、事前にSDKが依存するOSSについて精査できたのもこの本のおかげです。

上田 理 (2018). OSSライセンスの教科書,図9.2 OSSマトリョーシカ(OSSの中にOSSが入っている)

開発・リリースフローの検討

開発中の新機能や特定クライアントのissue等の開示は難しいため、当面はそれらの開発プロセスはPrivateにすることにしています。適切に公開範囲をコントロールするため、開発・リリースフローを次のリポジトリ・CI構成で行うことにしました。

Publicリポジトリではソースコードの公開だけでなく、issueやPRの公開・受付も行います。また、OSS化することを優先したため、当初PRの受付はバグ修正に限定しています。機能追加のPRは受け入れ体制が整い次第少しづつ受け付ける方針としています。体制が整った段階でCONTRIBUTING.mdを更新する予定です。

バージョニングポリシーの変更

v1では次の運用でバージョンアップを行っていましたが、この運用ではIFに破壊的な変更があるかどうか利用者側から影響範囲が分かりません。

  • 微修正はpatchバージョン
  • 機能追加などはminorバージョン
  • iOS/Android SDKのバージョンは揃える

OSS化するv2からは、バージョンアップがアプリケーションに与える影響を把握できるように、セマンティック バージョニング 2.0.0(以降 SemVer)に従うことにしました。これにより、バージョンアップが後方互換性を保った変更なのか、破壊的な変更なのか利用者からわかりやすくなります。

また、SemVerでメジャーバージョンの更新条件は自明となりますが、マイナーレベルのバージョニングの解釈に曖昧な点が残っていました。

マイナーバージョン Y (x.Y.z | x > 0)は...プライベートコード内での新しい機能の追加や改善を取り込んだ場合は、上げてもよいです(MAY)。その際にパッチレベルの変更も含めてもよいです(MAY)。

これについては、Reactのバージョニングポリシーなどを参考にチームで議論し次のように決定しています。

緊急性の高い不具合
即修正してリリースしなければならない問題については、patchバージョンを上げてリリースする。この場合は master ブランチから hotfix ブランチを切ってリリースする流れとなる。

緊急性の低い不具合(考慮漏れ・仕様バグなど)
minorバージョンを上げてリリースする。2通常のリリースフローでリリースする流れとなる。

バージョニングポリシーを明確化することで、迷いが減り迅速なリリースが期待できます。

v2 SDK(メジャーアップデート)で実施したこと

v1 SDKは、モノリシックなモジュールに多くの機能が詰め込まれており技術的負債が溜まっていました。また、利用者にとっての機能拡張性も十分に提供できているとは言えない状態でした。そのため、OSS化を良い機会と捉え、公開インターフェース及び内部実装の再設計を行い、保守性、拡張性などを高め長期的なSDKの品質維持が容易になるようにv1 SDKに対して非互換なアップデート(v2へのメジャーアップデート)を行う判断をしました。

モジュール化

v1では複数の機能が一つのモノリシックなモジュール内に詰め込まれていましたが、v2では各機能単位にモジュールを分割しました。

モノリシックなv1、機能毎にモジュール分割したv2

モジュール化には次のメリット・デメリットがあると考えましたが、デメリットを加味しても利用者が要件に応じた必要最低限のモジュールを選択できること、SDKの機能追加や拡張がしやすい状態に保つことは非常に重要だと考えました。

モジュール化のメリット

  • 要件に必要な機能だけをリンクして利用可能
  • 可読性の向上
  • バグやデグレの影響範囲分離・回避
  • 機能追加が容易(サードパーティによるモジュール追加も可能)

モジュール化のデメリット

  • 一部モジュール間連携用のAPI公開が必要
  • クライアントがどのモジュールをimportするか判断が必要
  • 全モジュール利用時のバイナリサイズは拡大する傾向
  • リリース等の作業が煩雑化

連携の方法について

モジュール分割により、モジュールが完全に独立して動くようになったわけではありません。各機能のモジュールはCoreモジュールに依存しており、Coreとその他のモジュールを連携させるための作業が必要になります。

この連携作業において、ツールによっては明示的な連携コードの実装が必要です。v2では、より簡単にSDKを導入できるようにモジュールがリンクされていれば自動的に連携するようになります。具体的にはCoreモジュールに各モジュールをlistenerとして登録し、各イベントを受け取る方式となります。尚、モジュールの自動登録には、各プラットフォームごとの機能を使用しています。

iOSでのモジュール連携内部実装

iOSではクラスがランタイムに追加された時に呼ばれるメソッド(load)があり、各モジュールのクラスがロードされたタイミングで、Coreモジュールにクラスを登録できます。リンクされたクラスは基本的に全てロードされます。

@implementation KRTInAppMessagingLoader

+ (void)load
{
    [KRTInAppMessaging _krt_load];
}

@end

Objective-CのloadからSwiftのload用関数を呼び出す

@objc(KRTInAppMessaging)
public class InAppMessaging: NSObject {
    ...
    @objc
    public class func _krt_load() {
        KarteApp.register(library: self)
    }
    ...
 }

Swift側でのモジュールの登録

Android (JVM)でのモジュール連携内部実装

JVMは基本的に初期状態ではリンクされたクラスはロードされず、呼び出し時に初めてロードされるため、iOSと同じアプローチは使えません(lazy load)。

JVMのClassLoaderにはリンクしたjar内の決められたファイルを参照する仕組みがあり、そちらを利用することで動的にリンクされたモジュールのロード・登録が可能です。

val libraries = ServiceLoader.load(Library::class.java)
libraries.forEach { library ->
    register(library)
    library.configure(self)
}

ServiceLoaderを使用したモジュールの登録

開発言語の変更

iOS / Android共に、メインの実装言語を各プラットフォームのよりモダンな言語に変更しました。iOSは Objective-C から Swiftへ、Androidは JavaからKotlinに変更しています。

言語変更のメリットを次のように考えています。

  • 可読性の向上
  • 言語の安全性向上
  • メンテナンス性の向上
  • 言語背景が(ある程度)似ているため、今までよりもプラットフォーム間の共通点が多い

一方デメリットは次の通りです。

  • 自由度の低下(安全性とトレードオフ)
  • バイナリサイズの膨張や呼び出しコスト増大
  • レガシーな開発環境のアプリでは、余計なものが増えてしまう

デメリットはレガシーな開発環境に限定されており、近年では完全にレガシーな環境のアプリは少ないと判断しました。

また、PLAIDではチームやプラットフォームの垣根なくコードを書くことが多いため、言語仕様が(ある程度)近いことによるメリットは大きいと感じています。

リファレンス自動生成

v1では、Developer Portal(Karteを利用する開発者向けのドキュメント置き場)のAPIリファレンスを、手動でメンテナンスしていました。

この方式は更新漏れでコードとドキュメントに乖離が生じやすい上に、バージョン毎の対応関係もわからずメンテナンスコストが高い状態でした。

v2からは、ドキュメンテーションコメントをソースコード内に記載し、リファレンスを自動生成することで、コードとドキュメントの乖離が起きにくくしました。iOSはrealm/jazzy、AndroidはKotlin/dokkaでドキュメントを生成しています。

また、バージョン毎にリファレンスが生成されるようになり、対応関係がより明確になりました。リファレンスはGitHubのリポジトリにPushして、iOS / AndroidともにGithub Pagesを利用して公開しています。

v2では、ここで紹介した他にも、サポートポリシーの刷新や新機能追加などを多く盛り込みました。興味があるかたは、ぜひリリースノートをご覧ください

OSS化とv2リリースを同時進行したことによる効果

v2へのメジャーアップデートとOSS化を同時にリリースしたことで、結果として「OSS化だけでは提供できない価値」である「機能性」「拡張性」「保守性」を付与できました。

これにより「SDK利用者の開発体験」全体を向上させることができインパクトのあるリリースになったのではないかと考えています。

おわりに

簡単にSDKをOSS化した背景、v2アップデートの概要、チームで取り組んだことを紹介しました。

まだまだ、OSS運用のノウハウは溜まっておらず、英語のみでのメンテナンスや、他開発者とのコラボレーション方法の整備に関してもスタート地点であり、その他にも多くの課題が残っています。

引き続き、透明性が高く開発者が安心して使えるSDKを提供するために改善を続けていきたいと思います。