こんにちは。お久しぶりです。株式会社ウォールオブデス代表のmasatojamesです。
この度、ロックに特化した機能を詰め込んだiOSアプリ「Rocket for bands II」をリリースしました。
創業期にしては技術要件がけっこう面白いのでご紹介させてください。
ためしにアプリも触りながら読んでみてください↓
https://apps.apple.com/jp/app/rocket-for-bands-ii/id1550896325
技術要件
フロントエンド
- Swift 5.2
- UIKit
- MVVM
- Combine
バックエンド
アプリケーション
- Swift 5.2 - Vapor 4.0 - Clean Architecture - Fluent ORM - SwiftNIOインフラ
- Docker - Kubernetes - Amazon EKS on EC2 - MackerelDB
- RDS Aurora for MySQL
主な特徴
Server Side Swift
なによりバックエンドでServer Side Swift Vaporを使用しているが特徴的だと思います。スタートアップの新規開発において導入している企業は日本初かもしれません。
世界的に使われている事例としてはAmazon Prime VideoのSmoke Frameworkというフレームワークがあります。今回弊社では、一番スター数も多く、機能も充実していたVaporを採択しました。
EKS
このServer Side SwiftをAmazon EKS(Elastic Kubernetes Service)というコンテナオーケストレーションサービスに載せて世に放ちました。EKSに載せたことによって、Kubernetesのエコシステムをフルに活用でき、運用にグッと安定感が増しました。
MVPでここまでやる要件は本当に少ない(大企業向けのSaaSくらいだと思います)し、このアプリに関してはどう考えてもtoo muchです。
僕が今まで培った技術スタックと、このサービスは今後10年スパンでスケールしていくことを見越して採択しました。あとは完全に趣味です。
Swift × Swift
↑HUNTER × HUNTERを意識しています。
さて。Swiftはとても素晴らしい言語です。値型中心の静的型付け言語として、とても多くの抽象化のための機能、型安全とパフォーマンスを兼ね揃えた言語仕様を持っています。
Swiftの良さがパッとしていない方はこれを読んでほしい↓
https://heart-of-swift.github.io
さてさて。そんなSwiftがアプリケーションの端から端まで跨っていることを想像してみてください。(嬉しすぎて発狂しますよね)
初期において特に嬉しかったことは
- いくつかのコードをフロントエンドとバックエンドで共有できた
- API仕様のためのドキュメントがいらなくなった
の2つです。2は1の副産物とも言えます。
Domain Entity(バックエンド) = Model(フロントエンド)
まず嬉しかったのは、フロントエンドでModelを用意する必要が全くなくなった点でした。バックエンドのDoain Entityとして使用しているコードをそのまま使うことができたからです。
例えば、以下のようなUserを定義するDomain Entityがあるとします。
public struct User: Codable, Identifiable, Equatable {
public typealias ID = Identifier<Self>
public var id: ID
public var name: String
public var biography: String?
public var thumbnailURL: String?
...
}
これはそのままフロントエンドのModelもUser
としての役割を果たします。
というか、これらの型の整合性は保たれなければならないので使い回すのが望ましいんですよね。あっち変えてこっちも変えてっていう作業を人間がやるとバグの元になるし、みんな普段よーーく注意してやっているんだと思います。
フロントエンド書いてて、
「うーんUser.nameがバックエンドに足りてないなあ」
「あれ、User.biographyってoptionalじゃなかったっけ?」
「User.thumbnailURLってURL?型だっけ?String?型だっけ?」
みたいな不思議を抱いて、バックエンドエンジニアに確認し、場合によっては修正を依頼した経験がありますよね。
使い回せてAPI仕様定義にもなるEndpoint
我々はもうひとつEndpoint
という層を責務分離し、バックエンドとフロントエンドでコード共有しています。これは何かというと、それぞれのエンドポイントを定義するレイヤです。
例として、以下のようなGetUserInfo
というEndpointを用意します。
// 自身のアカウント情報を取ってくるGETエンドポイント
public struct GetUserInfo: EndpointProtocol {
public typealias Request = Empty // request bodyの型
public typealias Response = User // responseの型
public struct URI: CodableURL {
@StaticPath("users", "get_info") public var prefix: Void // pathの定義。query string parameterやdynamic pathも定義できるので詳しくはGitHub見て。
public init() {}
}
public static let method: HTTPMethod = .get // HTTPメソッドの型
}
このエンドポイントは$ curl -H GET 'http://api.hogehoge.com/users/get_info' ...
という風にして叩くことができます。
ではこのstruct GetUserInfo
を読み解いていきたいと思います。
読めば分かる通り、この裏にはあるProtocolが用意されている必要があります。それを読めばOKです。
import CodableURL
// HTTPメソッド
public enum HTTPMethod: String {
case get = "GET"
case put = "PUT"
case post = "POST"
case delete = "DELETE"
}
// エンドポイントを抽象化したProtocol
public protocol EndpointProtocol {
associatedtype Request: Codable // リクエストボディ
associatedtype Response: Codable // レスポンスボディ
associatedtype URI: CodableURL // パス
static var method: HTTPMethod { get } // HTTPメソッド
}
// 空だよーて時に使う
public struct Empty: Codable {
public init() {}
}
このpubic protocol EndpointProtocol
を読むと、以下の型が必要とされていることがわかります。
- Request
- Response
- URI
- method
そう、これらはそのままエンドポイントに必要なリクエストボディ、レスポンスボディ、パス、HTTPメソッドを表しています。
偉い人はこういうのを最初にSwaggerや任意のドキュメントで用意するだろうと思います。僕たちは、このEndpointレイヤーを通じてこの仕様定義を直接することができます。
さらに、これをiOSでこのように使い回すができます。
import Endpoint // バックエンドで定義されたEndpointレイヤー
let req = Empty() // リクエストボディ
let uri = GetUserInfo.URI() // パス
var urlRequest = URLRequest(url: uri.encode(baseURL: "http://hogehogeapi.com"))
urlRequest.httpMethod = GetUserInfo.method.rawValue
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
...
guard let data = data else { return }
do {
let response = try JSONDecoder().decode(GetUserInfo.Response.self, from: data)
print(response) // ✔ { id: "xxxx-yyyy", name: "hogehogoe", biography: nil, thumbnailURL: "https://hogehoge.com/hogehoge.png" }
} catch let error {
fatalError(String(describing: error))
}
}
task.resume()
struct GetUserInfo
で定義されているものをそのまま参照できていることがよく分かると思います。これがあればフロントエンドとバックエンドの整合性が不足することは絶対にありません。
その他の嬉しいポイント
😁
- 技術スタックを統一できる
- 安全性が高い
- 抽象度が高い
- テスト性が高い
- Xcodeが使える
逆に困ったことは?
これも大事ですね。いくつかあります。パッと挙げると
ビルド時間が長い
何よりマルチコンテナビルドでもだいたい10分~15分くらいかかってデプロイが大変でした。
実例が少ない
少ないんですよねー
バグがあったりなかったり
マンパワーで解決した部分があったりなかったり
プロトタイプには向かない
スタートアップが2週間~1ヶ月でサクッとプロトタイプつくるのには絶対に向かないと思います。少なくとも現状では。これは完全に趣味の領域。遊び。楽しい。
殆どの処理をSwiftNIOのEventLoopFuture
モナドに包む必要があって、flatMap
とかオペレータをつなげていくと型推論が届かなくなることがある
これくらいですね。ただ、絶望的なものはなく、まあ耐えれるなあという所感でした。
最後に
いかがでしょうか。今すぐServer Side Swiftを書きたい衝動に駆られているのではないでしょうか。ぜひVaporの公式Tutorialから始めてみてください。
弊社では、そのうちwebもSwiftで書いたり、AndroidもSwiftで書いたり、インフラのIaCもSwiftで書いたりすると思います。Swiftで統治された幸せな世界に興味ある方はTwitterまで気軽にご連絡ください。もちろんServer Side Swiftの経験が無くとも大歓迎です。ゆるくカジュアルに話しましょう。
その他ご質問などもお気軽にお待ちしています。