iOSアプリのためのログインフレームワークを1から作り直した話

モバイルチームのオジマです。

サイボウズでは複数のモバイルアプリをリリースしています。モバイルチームではログインを司る機能をフレームワークとして切り出し、製品を横断して利用しています。今回は、そのログインフレームワークの作り直しについて紹介します。

既存のログインフレームワークの問題点

はじめに、フレームワークを作り直す原因となった既存のログインフレームワークの主な問題点について説明します。

既存のログインフレームワークは以下に示す問題を抱えています。

  1. エラーのハンドリングが困難な設計になっている
  2. 処理の流れを追いづらい
  3. ログインとは直接関わりのない責務まで担っている
  4. テストやドキュメントが十分に書かれていない

それぞれについて詳しく見ていきます。

エラーのハンドリングが困難な設計になっている

既存のログインフレームワークでは、主なエラーを階層構造にした上で直積型で定義しているため利用者側でのハンドリングに多くの手間を要しました。適切なエラーの型を得るためには、階層を適切に深堀り、適切な型でキャストする必要がありました。 また、エラーがメソッドの戻り値として返却される場合と共有インスタンスから返却される場合の2種類あるという問題もありました。さらにメソッドの利用者はそのメソッドがエラーを返すのかどうかを静的に判断できませんでした(これは使用しているサードパーティーライブラリに起因する問題ですが)。

これらの問題により、フレームワークの利用者は1つ1つの処理ごとに注意深くエラーハンドリングを考える必要がありました。

処理の流れを追いづらい

ログイン処理は以下のような形で提供されていました。

loginModel.login()

これは一見すると利用者から見てログインの機能が隠蔽されていて使いやすいようにも思えるのですが、この1つの命令の中で2要素認証やパスワード再設定の処理など全てを一連の流れとして内包しているため、今どのような状態にあるのかを把握することが容易ではありませんでした。また、フレームワーク内部の実装はモデル同士が複雑に依存しており読みづらいものでした。

ログインとは直接関わりのない責務まで担っている

ログイン時に用いた認証情報をローカルストレージに保存する処理や、保存された認証情報のマイグレーションなども既存のログインフレームワークは担っていました。これらはログインと間接的に関わりのある機能ですが、ストレージへの保存方法や保存する際のフォーマットは製品の裁量で実装を決めたいことが多く、これらの処理を完全にフレームワーク内で隠蔽することは困難でした。それぞれの製品で使いやすいようにメンテナンスをしてきましたが、コストの高い作業でした。

テストやドキュメントが十分に書かれていない

テストやドキュメントの整備はあまりされていませんでした。ハッピーパスのテストもおおむね書かれておらず、ドキュメントが整備されていないので人づてに挙動を確認することも度々発生していました。

新しいフレームワークのコードポリシー

上記の問題を改善するために新しく作り直すフレームワークでは3つのコードポリシーを定めています。

  • 簡単なフレームワークではなくシンプルなフレームワークにする
  • フレームワークを利用するサンプルアプリを提供する
  • 十分なテストとドキュメントを書く

簡単なフレームワークではなくシンプルなフレームワークにする

このポリシーは問題点の1., 2., 3. を解決するために定めています。

ここでの簡単なフレームワークとは、内部構造を知らなくても1つのメソッドで完結する構成を表しています。一方でシンプルなフレームワークとは利用者から見てこのメソッドを叩けば何が起こるのかが明らかな構成を表しています。

既存のフレームワークのように1つの大きなメソッドによって全ての処理を行い、また全てのエラーを大きくまとめるのではなく、いくつかの単純な処理を組み合わせて利用することでログインを行うフレームワークにしました。これにより、処理の流れが把握しやすくなりエラーのハンドリングも容易になると考えました。また、ログインとは直接関係のない処理はフレームワークが担うのではなく、各製品ごとに実装する方針としました。

フレームワークを利用するサンプルアプリを提供する

これは前述のポリシーを補強するために定めています。

シンプルなフレームワークになっているかを実装と合わせて実際に利用してみることで確認し、より使いやすいフレームワークを目指しています。また、サンプルアプリはドキュメントと合わせてフレームワークの挙動や利用方法を確認することにも役立ちます。

十分なテストとドキュメントを書く

このポリシーにより4. の問題点を解消します。

技術選定

コードポリシーを意識しつつ技術選定を行いました。

  • テストフレームワークとしてXCTestを用いる
  • ドキュメントはDocCで記述する
  • サンプルアプリはSwiftUIで提供する
  • 非同期処理機構としてasync/awaitを用いる
  • Linuxでビルド可能にする

XCTest、DocCとSwiftUIはチームメンバーが無理なく理解でき、またAppleが提供しているため今後も長期的なサポートが期待できるという理由で選定しました。非同期処理機構とLinuxでのビルドはそれぞれ以下のような理由から決定しました。

非同期処理機構としてasync/awaitを用いる

ログインフレームワークでは、ログイン基盤であるサーバーとの通信を行うため非同期処理は必須な処理です。非同期処理機構として候補は3つありました。

  1. RxSwift
    kintoneモバイルで利用実績があるフレームワークです。

  2. Combine
    Office 新着通知で利用実績があるフレームワークです。

  3. async/await
    サイボウズのプロダクトで利用実績はありませんが、Swiftの言語機能として注目していました。

RxSwiftとCombineはそれぞれサイボウズのプロダクトで利用実績があるフレームワークでしたが、Swiftの言語機能として今後も安定したインターフェースが提供されることや最新の技術の知見を貯めるという観点から、今回はasync/awaitによる実装を行うことにしました。

Linuxでビルド可能にする

この方針は、主にCI環境の利用金額に依存した決定です。

現在モバイルチームでは複数のプロダクトでGithub Actionsを主なCI環境として利用しています。Github ActionsにおけるmacOSインスタンスとLinuxインスタンスの単位時間あたりの利用金額は10倍の差があります。利用金額に気を取られ書きたいテストが書けないという状況は避けなければなりません。

そのため、今回はできる限りLinuxでビルド可能なフレームワークとして開発することにしました。

作り直しを行った成果

作り直しを行った新しいログインフレームワークでは、ログインを以下の流れで行えるようになりました。

let sessionProtectionDetector = SessionProtectionDetector()
let protection: ProtectionMethod
do {
    protection = try await sessionProtectionDetector.detect(from: URL(string: "https://example.com")!)
} catch {
    ...
}

let credential: Credential
switch protection {
case .none:
    let authenticator = UsernamePasswordAuthenticator()
    credential = try! await authenticator.authenticate("username", and: "password")
case .clientCertificate:
    let authenticator = ...
    credential = ...
}

これまでのログインフレームワークでは1つの巨大なメソッドで表現されていた部分を、役割ごとにそれぞれ分けることで、利用者が処理の流れを追いやすいようになりました。また、メソッドごとにthrowsでエラーを返すようになったためエラーハンドリングも容易になりました。

さらに、ログイン以外の処理をフレームワークに含めず製品側での実装を必要とする形にしたため、フレームワークの機能追加や修正などを他製品と協調しながら行う必要がなくなりました。

今回の作り直しによってユニットテストやドキュメント、サンプルアプリを充実させたので、実際に製品に取り込む際の悩むポイントも改善されたのではないかと期待しています。

実装を通して得た学び

ドキュメントを書く大切さ

今回の作り直しでは、フレームワークを利用する際やフレームワーク自体のメンテナンスの際の手助けとなるために、ドキュメントの整備を心がけました。

コードと共にそれにまつわるドキュメントを書くことは、利用時やメンテナンス時のメリットはもちろんありますが、それ以外のコードを書くという視点でもよい効果があると感じました。ドキュメントが書きづらく自然言語で説明しづらい部分のコードは、変数名やデータ構造・アルゴリズムが最適ではないかもしれないとリファクタリングのきっかけになることが多くありました。ドキュメントをきっかけに行ったリファクタリングはおおむねよいものばかりだったと感じています。

今後は、フレームワーク以外のプロダクトコードでもドキュメントを書く試みを模索したいです。

Linuxビルドを担保する難しさ

技術選定の段階から、フレームワークをLinuxでビルド可能にすることを目標としていました。LinuxビルドとmacOSビルドの大きな違いはFoundationライブラリの実装差異です。macOSでビルドする場合、Objective-Cで実装されたFoundation(Cocoa Foundation)を利用しますが、LinuxビルドではCで実装されたFoundation(Core Foundation)を利用します。おおむね同等の機能が提供されていますが、いくつか利用できない機能も存在します。

Core Foundationを利用する上で、URLSessionのasync/awaitインターフェースがまだ実装されていないことや、クライアント証明書を利用したネットワーク通信をサポートしていないことは、今回の作り直しにおいて大きな障害でした。これらの障害はそれぞれasync/awaitインターフェースのextensionによる追加実装とクライアント証明書を利用したネットワーク通信のモック化により回避しましたが、解決に多くの時間を要してしまったのは反省点だと感じています。

まとめ

この記事では、モバイルチームで利用しているログインフレームワークの作り直しの取り組みについて紹介しました。今後は、新たに作ったログインフレームワークを各製品に取り込んでいき、使い心地を確認していく予定です。取り込む過程でより良いフレームワーク改善を進めていきたいと考えています。