AndroidアプリにViewModelを導入しました

はじめまして。Misocaモバイルチームのtijinsです。

この記事は、弥生アドベントカレンダー14日目の記事です。

MVPからMVVMへ

Android版MisocaはModel-View-Presenter構造で作られていたのですが、Model-View-ViewModel構造へのリファクタリングが完了しました。

コードがスッキリしたので、リファクタリングの内容を紹介します。

MVPパターンとMVVMパターン

Model-View-Presenter(MVPパターン)

MVPパターンでは、PresenterがViewの参照(intrefaceで)を保持し、結果をViewに表示します。

Viewを直接参照せずにInterfaceにしておくことで、PresenterからViewの依存を排除しています。

@startuml
  interface PresenterResult
  class View implements PresenterResult
  {
    表示用データ
  }
  class Presenter
  class Model

  View --> Presenter: 読み込み・新規作成等の操作
  Presenter --> PresenterResult : 結果の表示
  Presenter --> Model
@enduml

Model-View-ViewModel(MVVMパターン)

MVVMパターンでは、ViewModelからViewの参照は不要です。

表示用のデータはViewModelが保持しており、ViewがViewModel上のデータを監視(Observe)して表示します。

@startuml
  class View
  note right of ViewModel : ViewModelは、操作結果により、\n保持する表示用データを更新する
  class ViewModel{
    表示用データ
  }
  class Model

  View --> ViewModel: 読み込み・新規作成等の操作\n変更の監視(変更があれば表示を更新する)
  ViewModel --> Model
@enduml

ViewModel化のメリット

ViewModelにも色々あるみたいなのですが、ここでのViewModelはデータを保持できるPresenterといった感じです。

PresenterからViewの参照を排除できる

ViewModel化前は、Presenterに非同期処理完了のコールバックとして、Viewの参照を(interfaceとしてですが)渡していました。

ViewModelを利用すると、ViewModelからViewへの参照は不要になります。

AndroidアプリでのViewModelのメリット

  • FragmentでもActivityライフサイクルでのデータ保持が可能

Fragmentにデータを保持していると、画面の回転等でFragmentが破棄された際に、データの読み込みが必要でした。

ActivityライフサイクルのViewModelを利用する事で、Fragmentが破棄されても、データの引き継ぎが可能になります。

  • ライフサイクル管理の単純化

Presenterが行う非同期処理の結果をFragmentで表示する場合、先にFragmentが破棄されていると、クラッシュしてしまう問題がありました。

ViewModelのライフサイクルはFragmentから分離されている為、Fragmentの状態を気にせずに、非同期処理が可能です。

リファクタリング前のコード

最初にリファクタリング前のコードです。

Viewを直接参照しないのは、PresenterからViewの依存を排除する為です。

こうしておくと、テスト用のダミークラスに差し替えたりできます。

@startuml

  interface ItemsResult
  {
    onItemLoaded(items)
    onFailed(message)
  }
  
  class ItemsFragment implements ItemsResult
  {
    items:List<Item>
  }
  class ItemsPresenter
  {
    loadItems(page)
  }
  class UseCase

  ItemsFragment --> ItemsPresenter: itemsの読み込み操作
  ItemsPresenter --> ItemsResult : 結果(items)
  ItemsPresenter --> UseCase


@enduml

ItemsViewInterface.kt

interface ItemsResult
{
    fun onItemsLoaded(items:List<Item>)
    fun onFailed(message:String)
}

ItemsPresenter.kt

class ItemsPresenter(
  private val context:Context,
  private val coroutineScope: CoroutineScope,
  private val view: ItemsResult) {

    private val useCase = ItemsUseCase()

    fun loadItems(page:Int) {
        coroutineScope.launch{
            try {
                val result = useCase.loadItems(context, page)
                view.onLoadItems(result)
            } catch (ex: Exception) {
                view.onFailed(ex.message)
            }
        }
    }
}

ItemsFragment.kt

class ItemsFragment:Fragment,ItemsResult
{
    // 請求書一覧のアダプターです
    private var adapter: ListAdapter<Item, RecyclerView.ViewHolder>?
    private var presenter: ItemsPresenter?
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        presenter = ItemsPresenter(requireContext(), viewLifecycleOwner.lifecycleScope, this)
        presenter?.loadItems(1)
    }
    
    override fun onItemsLoaded(items:List<Item>){
        adapter?.submitList(items)
    }
    
    override fun onFailed(message: String){
        // エラー表示する
    }
}

リファクタリング後のコード

ViewModelの実装

ViewModelからFragmentへの参照が無くなり、すっきりしていると思います。

@startuml

  class ItemsViewModel
  {
    - itemsBuffer:List<Item>
    + items:MutableLiveData<List<Item>>
    + networkState:MutableLiveData<NetworkState>
    laodItems(page)
  }

  note right of Fragment: Items, networkStateの変更を監視している
  class Fragment
  class UseCase

  Fragment --> ItemsViewModel : itemsの読み込み
  ItemsViewModel --> UseCase

@enduml

ItemsViewModel.kt

MisocaのAndroid版では、APIとの通信にContextが必要な為、AndroidViewModelを継承しています。

AndroidViewModelを継承することで、ApplicationContextをgetApplication()で参照可能になります。

ただし、ApplicationContextには言語設定や画面向きの変更が反映されない為、ViewModel内でUIに関する処理を行わないよう注意が必要です。

class ItemsViewModel(context: Context) : AndroidViewModel(context.applicationContext as Application){
    // 通信中状態、エラーの通知用のobserve可能なプロパティ
    val networkState = MutableLiveData<NetworkState>()    
    // 表示するデータの実体(LiveData経由でFragmentに渡す)
    private val itemsBuffer = ArrayList<Item>()
    // observe可能なプロパティ
    val items = MutableLiveData<List<Item>>()
    
    // データの一覧を読み込みます
    fun loadItems(page: Int) {
        viewModelScope.launch {
            try {
                networkState.postValue(NetworkState.LOADING)
                
                // loadItemsはDispatchers.IOで動作するsuspend functionです。
                val result = useCase.loadItems(getApplication(), page)
                itemsBuffer.addAll(result)
                
                // observeしているViewに更新を通知する
                items.postValue(itemsBuffer)
                networkState.postValue(NetworkState.LOADED)
            } catch (ex: Exception) {
                // エラーメッセージを表示
                networkState.postValue(NetworkState.error(ex.message))
            }
        }
    }
}

NetworkStateの実装は、下記のコードを参考にしました。

https://github.com/android/architecture-components-samples/tree/master/PagingWithNetworkSample

ViewModel上のデータ変更を監視する

ViewModelをFragmentから使う場合は、fragment-ktxにあるExtension(viewModels,activityViewModels)を使用すると便利です。

build.gradle

dependencies {
    implementation "androidx.fragment:fragment-ktx:$fragmentKtxVersion"
}

ItemsFragment.kt

class ItemsFragment:Fragment
{
    // fragment-ktxのactivityViewModelsでviewModelを取得します
    private val viewModel: ItemsViewModel by activityViewModels()
    // 請求書一覧のアダプターです
    private var adapter: ListAdapter<Item, RecyclerView.ViewHolder>?

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)

     observeViewModel()
     // itemsの読み込みを開始します
     viewModel.loadItems(1)
   }

    private fun observeViewModel() {
        // itemsの更新を監視します
        viewModel.items.observe(viewLifecycleOwner) { items ->
            // List<Item>をそのまま通知しても、DiffUtilが効率的に再描画してくれるので気にしない
            adapter?.submitList(items)
        }
        
        // 通信中状態の更新を監視します
        viewModel.networkState.observe(viewLifecycleOwner) {
            when (it.status) {
                Status.RUNNING ->{
                    // プログレスバーを表示
                }
                Status.FAILED -> {
                    // エラー表示
                }
                Status.Success -> {
                    // プログレスバーを消す
                }
            }
        }
    }
}

おまけ

この画面は2つのタブがそれぞれFragmentになっているのですが、ViewModelを通じて2つのタブがデータを共有しています。

タブ間(Fragment間)でのデータの移動も、ViewModelを使用する事で、簡単に実現できています。

スクリーンショット 2020-11-13 10.37.55.png (47.6 kB)

まとめ

Viewの操作が不要になって、スッキリしました

宣伝

Misoca 開発チームでは、モバイルアプリエンジニア(iOS, Android)を募集しています。

www.wantedly.com

君のプログラミング言語は輝いているか

こんにちは、Misoca開発チームの黒曜(@kokuyouwind)です。

そしてこの記事は弥生 Advent Calendar 2020の 11日目の記事です。

昨日の担当はkosappiさんのCodePipelineのステージ間で変数を受け渡しするでした。

プログラミング言語について

Misoca開発チームでは主にサーバーサイド側言語としてRuby、クライアントサイド言語としてTypeScriptを利用しています。

一方で弥生製品開発チームは主にC#を利用しており、最近ではTypeScriptやGo言語も一部で利用しています。

そうなってくると、気になるのは「今後新しいプロダクトを作るとしたら、プログラミング言語は何にするべきなのか?」ということです。

今回はそれぞれの言語の特徴や向いている用途について、ざっくりまとめていきたいと思います。なお極力資料を探しながら正確に書こうと思いますが、個人的な印象やイメージが入ってしまう部分もあるかと思いますのでご了承ください。

なお誕生時期や特徴の主な出典はプログラミング言語図鑑に拠ります。

プログラミング言語図鑑

プログラミング言語図鑑

  • 作者:増井敏克
  • 発売日: 2018/04/13
  • メディア: Kindle版

目次

Ruby

言語の特徴

Misoca開発チームがバックエンド開発に使っている言語です。日本発で個人開発から始まったという、割とレアな出自を持っています。また公開が1995年と、実はC#より長い歴史を持つ言語です。

Rubyの特徴として、C++に代表されるクラスベースのオブジェクト指向よりも、Smalltalkに代表されるメッセージベースのオブジェクト指向を強く意識していることが挙げられます。

多くの言語では、数やそれ以外のプリミティブな型はオブジェクトではありません。 ですが、RubyはSmalltalkの影響を受け、すべての型がメソッドやインスタンス変数を与えられるようになっています。 これがRubyが使いやすい理由の一つです。 Ruby公式ページ - Rubyとは より

この特徴と、メッセージ呼び出しやブロック渡しでの柔軟な構文によって、Rubyでは自由度の高い言語内DSLを定義することができます。

例えばRailsの routes.rb ファイルは以下のように書くことができますが、これは妥当なRubyコードになっています。*1

Rails.application.routes.draw do
  resources :brands, only: [:index, :show] do
    resources :products, only: [:index, :show]
  end

  resource :basket, only: [:show, :update, :destroy]

  resolve("Basket") { route_for(:basket) }
end

他にも関数型プログラミングの要素が取り入れられていたり、オープンクラスとオーバーライドによる柔軟なクラス拡張が行えたりなど、とにかく「プログラマが自然に書きやすい」ことを重視したまつもとひろゆき氏の思想が伺えます。

向いている用途

最も普及している用途はRuby on RailsによるWebプログラミングで、特にスタートアップなどの小規模かつ迅速な開発で強みを発揮します。

他に言語内DSLの特性を活かしてVagrantChefなどの設定ファイルでの利用が有名です。またサーバ内で実行するバッチ処理などのスクリプト用途でも手軽に書きやすく重宝します。

一方で、自由度や動的な型システムによって大規模開発では混乱を生みやすい言語だといえるでしょう。また実行速度の面では遅い部類に入るため、処理速度が重要な場合はあまり向いていないと言えます。

なお12月25日(クリスマス!)にリリースされる予定のRuby 3.0*2では静的型検査システムの導入や実行速度の改善などが行われているため、これらの懸念点が多少緩和されるのではないかと期待しています。

C#

言語の特徴

弥生製品チームが主に利用している言語です。マイクロソフトが2000年に公開した、汎用目的のオブジェクト指向言語になっています。

一般に使われ始めたのは、Visual Studio .NETに組み込まれた2002年からになりそうです。それまでのネイティブビルドがターゲットだったVisual Studioシリーズ(6.0まで)から、.NET FrameworkをターゲットとしたVisual Studio .NETに移行するタイミングで追加されただけあり、.NET Frameworkの標準的な言語という扱いを受けています。*3

C++の構文をベースにしつつも、低レベルのメモリアクセスは基本的に制限したりガベージコレクションが標準で利用されるなど、自分の足を撃ち抜くことがないようになっています。型システムは.NETの共通型システムに依りますが、おおむねclassやinterfaceを用いたクラスベースのオブジェクト指向といえるでしょう。

またC# 3.0で追加されたLINQ用のクエリ式やC# 7.0で追加された関数型プログラミング由来のパターンマッチ機能など、積極的に様々な機能が導入されています。

特にクエリ式によって、データベースに対するクエリ発行を以下のように言語内で記述できる点は特筆すべきでしょう。*4

IEnumerable<string> highScoresQuery2 =
    from score in scores
    where score > 80
    orderby score descending
    select $"The score is {score}";

ほぼSQLと同じ内容を言語内で記述できるため、構文検査や静的な型検査もコンパイル時に行えるのは強みといえます。*5

向いている用途

.NET Frameworkの主力言語であるため、Windowsデスクトップ開発においては最有力候補と言えます。またUnityでのスクリプティングにおいてもC#が採用されています。

.NET ASPなどを利用したWebプログラミングも可能で、大企業やSIerなどの大規模プロジェクトで利用されているように思います。

一方で、やや文法が古くボイラープレートコードが多くなること・開発環境が実質的にVisual Studioに制限され自由度が少ないこと・稼働環境にWindowsサーバライセンスが必要になることなど諸々の問題から、スタートアップなど小規模なWebアプリケーションでの採用は稀という印象です。

またWindowsや.NET Framework自体が重厚なため、昨今のコンテナ化・マイクロサービス化の文脈ではコンテナサイズやメモリ使用量が膨らみやすいという点で他言語に軍配が上がりやすいと言えるでしょう。

こうした状況はMicrosoftも把握しているようで、.NET Coreのマルチプラットフォーム対応や.NET 5での単一の実行可能バイナリ生成など、着々と改善を重ねています。BlazorによるWebAssemblyアプリ作成などもあり、今後の展開によっては小規模Webサービスやマイクロサービス用途でも選択肢に挙がってくるかもしれません。

Go

言語の特徴

弥生基盤チームが一部で利用している言語です。Googleが2009年に公開した汎用目的のプログラミング言語です。

Goの基本的な言語仕様は単純で、個人的にはBetter Cという印象を受けます。ただしinterfaceを利用したオブジェクト指向機能と、GoRoutineを用いた並列処理機構の2つは非常に特徴的です。

Goのオブジェクト指向ではクラス定義を行うことはできず、どんなメソッドを持つかのinterfaceのみを定義することができます。そしてある型がinterfaceのメソッドを全て定義していれば、その型はinterfaceを実装しているとみなされます。

例えば、sort.InterfaceLen() int, Less(i, j int) bool, Swap(i, j int) の3つのメソッドを要求するため、これらを定義した型であればsort.Sortに渡して整列することができます。いわゆる構造的部分型ですね。Rubyでよく言われるダックタイピングのような振る舞いを記述できますが、コンパイル時にインターフェイスを満たしているか静的に検査できる点が優れています。

GoRoutineはマイクロスレッドを立ち上げるための言語機構です。Erlangのマイクロスレッドに近いですが、各プロセスがメッセージボックスを持つアクターモデルではなく、チャネルを通じて同期的に通信を行うCSPスタイルとなっています。

また標準でコードフォーマッターや単体テストライブラリなど組み込みのエコシステムが整備されており、コンパイルすると単一実行ファイルが生成されるため、コンテナベースのCI/CDといったモダンな開発プロセスに適応しやすくなっています。

向いている用途

Googleが自社向けに設計しただけあって、コンテナを利用したマイクロサービスシステムでは最初に名前が挙がる言語でしょう。ユーザが直接触れるWebアプリよりも、マイクロサービス内部のAPIサービスで特に強みを発揮するイメージです。また静的型システムを持ち並列処理機構も言語標準で持つため、Dockerなどミドルウェアの開発でも積極的に採用されています。

一般的なWebアプリでの利用も十分可能だと思いますが、Webフレームワークはデファクトが固まっておらず、フロントエンドも通してカバーできるTypeScriptに軍配が挙がっている印象です。もっとも、標準ライブラリのみで簡単なWebアプリなら作成できてしまうため、Webフレームワークの必要性が低いという事情もありそうです。

TypeScript

言語の特徴

Misoca開発チームがフロントエンド開発で利用している言語です。また弥生製品チームでも一部で利用されています。マイクロソフトが2012年に公開した、いわゆるAltJS言語の一種です。

JavaScriptに型検査システムやスコープ制限などの機能を追加した言語で、JavaScirptの厳密なスーパーセットになっているため既存のJavaScriptソースをそのまま利用でき移行が容易という点で他のAltJS言語と一線を画しています。

また型システムも非常に特徴的で、典型的なプリミティブ型・オブジェクト型などに留まらず、リテラルの値に依存するリテラル型や型レベル関数など意欲的な仕様が取り入れられています。

例えば、以下のように方位を表す "North", "East", "South", "West"のいずれかのみを取る型 CardinalDirection を定義することで、 move にそれ以外の文字列が渡されないことを型レベルで保証できます。*6

type CardinalDirection =
    | "North"
    | "East"
    | "South"
    | "West";

function move(distance: number, direction: CardinalDirection) {
    // ...
}

move(1,"North"); // Okay
move(1,"Nurth"); // Error!

一方で、型注釈を間違うと容易に型の整合性を崩せるなど健全性は諦めている部分があり、型検査を実用的なレベルで妥協するという割り切りが感じられる言語です。

向いている用途

現状のフロントエンド開発ではデファクトスタンダードと言えるでしょう。またサーバーサイドJavaScriptエンジンであるNode.jsを組み合わせることでサーバサイド開発でも利用することができます。

特にフロントエンド・サーバサイドを跨いだJavaScriptフレームワークであるNext.jsNuxt.jsなどは迅速な開発が必要なスタートアップなどで非常に人気があり、TypeScriptと組み合わせて採用するケースが増えているように思います。

静的型検査があるため開発規模が大きくなってもある程度はスケールしますが、コンパイルターゲットがJavaScriptであり速度面では他言語に劣ること、標準で利用されるnode_modulesが肥大化しやすくコンテナイメージが膨らみやすいことなどから、大規模開発では他の言語に分があるといえそうです。

とはいえTypeScript界隈はコミュニティが非常に活発であり、新たなツールでこうした課題が改善されていく可能性は十分ありそうに思えます。

その他の言語

他に、個人的に最近使われていると感じたり気になっている言語は以下のとおりです。

  • Swift : iOS開発開発のデファクトスタンダード。サーバーサイドSwiftという選択肢もあるが、事例は少なそうに感じる
  • Kotlin : Androidアプリ開発のデファクトスタンダード。サーバーサイドKotlinという選択肢もあるが、やはり事例は少なそうに感じる
  • Scala : オブジェクト指向と関数型プログラミングをいい感じに組み合わせたJVM言語。書ける人が書くと非常に生産性が高いが、設計方針が統一されていないとパラダイムが混ざってカオスになる。あとimplicit parameterを多様するとコンパイルがどんどん重くなる
  • Rust : 所有権管理が厳格なため、低レイヤー処理を安全に記述できる言語。ミドルウェアやドライバーなどハードウェア寄りのソフトウェアで特に強みを発揮するイメージ
  • F# : .NETに対応した関数型言語で、OCamlの文法をベースにしている。型プロバイダなど表現力が高いが、Scala以上に関数型寄りのパラダイムを持つためOCaml・Haskell辺りの経験がないと学習コストが高い

まとめ

いろいろなプログラミング言語がありますが、それぞれに設計者の思想や目的がある以上、用途に合わせてプログラミング言語を選ぶことで特徴を活かして輝かせたいですね。(雑なタイトル回収)*7

弥生 Advent Calendar 2020、明日の担当はmasuda_Mtenさんです。お楽しみに!

*1:routes.rbのコードはRailsガイド - Rails のルーティングから引用

*2:現時点ではRuby 3.0 preview 2がリリースされており、試すことができます。

*3:J#? 知らない子ですね…

*4:コードはC# - クエリ式から引用

*5:LINQやクエリ式はVB.NETやF#でも利用できるので、.NET系の言語共通の強みと言ったほうが正確かもしれません。

*6:コードはTypeScript Deep Dive 日本語版 - リテラル型から引用

*7:蛇足ですが、タイトルの元ネタは「君の青春は輝いているか」です。

CodePipelineのステージ間で変数を受け渡しする

この記事は弥生 Advent Calendar 2020 の10日目の記事です。

こんにちは。@kosappi です。最近は SRE としてインフラの改善に取り組んでいます。

Misoca 開発チームでは AWS を利用してインフラを構築しています。 AWS CodePipeline で使える変数の受け渡しが便利だったので、今回はこれを紹介します。

CodePipeline の基本機能について知りたい方は AWS による CodePipeline の紹介をご覧ください。

AWS CodePipeline(継続的デリバリーを使用したソフトウェアのリリース)

CodePipeline における変数の受け渡し

CodePipeline では各アクションの成果物をアーティファクトでやりとりできます。 近頃は、これに加えて「変数」という成果物も扱えるようになっています。

参考: AWS CodePipeline で実行中のアクション間での変数受け渡しが可能に

なお、AWS から提供されている変数についての資料は下記の2つです。

まとめ

  • アクションは変数を作成することができる
    • アクションによっては作成できない場合もある(後述)
  • 変数を作成するには名前空間を指定する必要がある
    • アクション毎に名前空間が必要
    • 名前空間が指定されていない場合は参照することができない
  • 作成された変数は #{NAMESPACE.VARIABLE_NAME} で参照できる

変数を作成することができるアクション

下記のアクションではユーザが変数を作ることができます。

  • CodeBuild
  • AWS CloudFormation
  • Lambda

上記以外のアクションではユーザは新たに変数を作ることはできず、定義済み変数のみ利用できます。 どのような変数が利用可能かは公式のドキュメントに一覧で載っています。

パイプラインアクションで使用できる変数

なお、定義済み変数を利用する場合でも名前空間の指定は必要です。

変数を下流ステージに提供する(CodeBuildの場合)

それでは、実際に CodeBuild で変数を下流ステージに提供していきましょう。 全体のイメージはこんな感じです。

f:id:rkosaka:20201207172506p:plain
パイプライン全体図

名前空間を指定する

名前空間を指定します。 コンソールからだと、ここにあります。

f:id:rkosaka:20201207173137p:plain
CodeBuild設定画面

CLI や terraform 等からもできますが、ここでは割愛します。 今回は CodeBuildVariables という名前空間にします。

buildspec 内で変数を作成する

buildspec 内で変数を作成するには env/exported-variables を利用します。 env では他にも変数を指定できますが、下流ステージから参照できるのは exported-variables のみです。

今回は BUILT_IMAGE_TAG という変数を作ります。 ここにビルドする際のタグ名を書いて、下流ステージでタグを使えるようにするイメージです。

version: 0.2

env:
  exported-variables:
    - BUILT_IMAGE_TAG
phases:
  pre_build:
    commands:
      - ...
      - TZ=Asia/Tokyo date +"%Y-%m-%d-%H-%M-%S" > ./TIMESTAMP
      - export BUILT_IMAGE_TAG=FUGAFUGA.`cat TIMESTAMP`
      - ...
  build:
    commands:
      - ...
      - docker build -t $BUILT_IMAGE_TAG -f ./Dockerfile .
      - docker push $BUILT_IMAGE_TAG
      - ...
  post_build:
    commands:
      - ...
artifacts:
  files:
    - ...

提供されている変数を参照する

下流のステージでは変数を参照するには #{NAMESPACE.VARIABLE_NAME} のように書きます。 今回は名前空間が CodeBuildVariables で変数名は BUILT_IMAGE_TAG なので #{CodeBuildVariables.BUILT_IMAGE_TAG} と書けば参照できます。

この書式で各種項目を書いてあげれば、動的に値を指定することができます。

例えば、CodeBuild の編集画面では、環境変数に上流から提供された変数を指定することができます。

f:id:rkosaka:20201208150938p:plain
上流ステージの変数を指定する

最後に

CodePipeline の隠れた(と個人的には思っている)便利機能である、変数を紹介しました。 ステージ間で情報をリレーしたいけど、アーティファクトでは立派すぎる...という場面で重宝するのではないでしょうか?

余談ですが、今回紹介した変数についてのドキュメントの日本語訳が非常に読み辛い状態になっています。 なんとなく、機械翻訳したものがそのまま使われているような気配を感じるのですが...。

変数の操作

宣伝

Misoca 開発チームでは AWS の便利な機能を使ってインフラを改善してくれるエンジニアを募集しています。

アプリケーションの不調を発見し、チームで改善できた話

弥生 Advent Calendar 2020 8日目の記事です。

開発本部の id:mizukmb です。普段はMisocaのインフラの面倒を見たりしています。

Misocaの開発チームでは毎週SLOの状況を共有する時間をとっています。SLOを設定した話は別記事で紹介しています。

tech.misoca.jp

先日、開発チームに共有したところ普段よりもレスポンスタイムが悪化してることがわかりました。これ以上の悪化はサービスに大きな悪影響を及ぼしてしまう事も懸念されました。

そこで、年末年始をトラブルなく過ごせるように開発チームでボトルネックを見つける会を開き、原因の特定から実際に改善し効果が表われたことを確認できました。

問題発見

Misocaには時期によってアクセス数の波があります。月末・月初はアクセス数が普段と比べて多くなり、レスポンスタイムもそれに伴って増加しやすい傾向にあります。

下図を見てもらうとわかりますが、9, 10月は連続してレスポンスタイムの95, 99パーセンタイル値が増加しています。さらに、9月は四半期締めの時期で特にアクセス数が多い月なのですが、それよりも10月の方がレスポンスタイムが長くなってしまっています。この時点では原因は特定できてなかった状態で、早急に手を打つ必要がありました。

f:id:mizukmb:20201207164639p:plain
Redashで可視化しているレスポンスタイムのグラフ。濃い赤が95、薄い青が99パーセンタイルのレスポンスタイム値。9月と比べて10月の方が大きくスパイクしていることがわかる

しかし、開発チームメンバーは各自プロジェクトのタスクを抱えており、どのくらいのタスクが発生するかわからないパフォーマンス改善を各自が率先して引き受けるのは心情的に難しい状態でした。

そこで、過去に開発者ブログでも紹介した「お気持ち会」の要領で有志で集まり、最低限下記を明らかにすることにしました。

  • レスポンスタイム悪化のボトルネック特定
    • フロントエンド、アプリケーション、バックエンド、ネットワーク等様々な観点から見つける
  • ボトルネックを解消する方針の策定

上記までを会のゴールとして、実際に解消する作業は別途行うこととしました。

f:id:mizukmb:20201204140203p:plain
開発チームでどうするか話し合って、有志でボトルネックを見つける会を開くことが決まった

お気持ち会の記事はこちら。

tech.misoca.jp

ボトルネックを見つける会

後日、ボトルネックを見つける会を開きました。当日はRedashで可視化しているレスポンスタイムのグラフやSkylightでRailsのアクション毎のレスポンスタイムやイベントシーケンスを確認しながら、ボトルネックとなっている処理をみんなで探しました。結果として、いくつかボトルネックと思われる処理の特定ができ、さらにはそれに対する具体的な改善策まで話し合って決めることができました。

f:id:mizukmb:20201207143141p:plain
当日の議事録。当日は多くの人に参加してもらったことで一般的な高速化のテクニックからドメイン固有の知見を活かしてボトルネックと思われる箇所を複数発見できた

最終的に、この見つける会ではボトルネックを思われる箇所をいくつかリストアップし、その中から特に大きな効果を期待できそうな改善策を決めて修正するというところまで決めて終わることができました。

コードの修正〜リリース後の効果測定

見つける会では今後の作業者のアサインまでは決められませんでしたが、修正箇所の実装に詳しいメンバーが積極的にタスクを拾い上げてくれたおかげで、会を開いたその日に修正のプルリクエストが出来あがるという驚きの仕事の速さでした。

f:id:mizukmb:20201204150658p:plain
頼れる開発チームメンバー

f:id:mizukmb:20201207143032p:plain
本当に2時間後にプルリクエストを作っていた

リリース直後から効果は表れていて、効果の高いところだと普段よりも95パーセンタイル値で40%程高速になっていました。

f:id:mizukmb:20201207174846p:plain
Skylightのレスポンスタイムグラフ。薄い赤が95パーセンタイル値。リリース直後からグラフが全体的に下がっていて改善されていることがわかる

山場だったアクセス数が増加する期間も大きくスパイクすることがなくなり、パフォーマンスの改善効果があったことを確認できました。

f:id:mizukmb:20201207182411p:plain
Redashで可視化しているレスポンスタイムのグラフ。濃い赤が95、薄い青が99パーセンタイルのレスポンスタイム値

まとめ

アプリケーションのレスポンスタイム悪化から、実際に開発チームで改善するまでの流れを紹介しました。

ボトルネックの発見方法については、過去にISUCON 1 というコンテストに参加していた個人的な経験が活きたかなあと思いました。

また、こうした緊急度の高い問題に対してきちんと開発チームとして問題の提起から解決、結果の測定まで手を動かした事も良かったですし、それをしっかり実行できるMisocaの開発チームは強いチームだなあと改めて実感しました。

採用

チームで問題解決に取り組むことができるMisocaに興味のある方からの応募を待っています!


  1. 与えられたアプリケーションの高速化を競うコンテスト http://isucon.net/

Serverless Framework で DocBase の日報を Slack に流す Bot を作った話

この記事は弥生 Advent Calendar 2020の1日目の記事です。

こんにちは。@KawamataRyoです。
最近嬉しかったことは、6 年ぶりにリアルの新刊を読めたことです。

さて、Misoca開発チーム では DocBase に日報を書くのですが、みんなの日報を読むために毎回ブラウザで DocBase を開くのが地味に面倒でした。 なので、日報の内容を Slack に流す Bot を作ってみました。
この記事ではその実装方法を紹介します。

🍿 何を作った?

DocBase に日報を投稿すると、こんな感じにフォーマットされたメッセージを Slack の専用チャネルに通知する Bot です。DocBase 上でのコメントの投稿にも対応しています。

f:id:ba068082:20201130135321p:plain

一応 DocBase 自体で Slack 連携に対応しているのですが、以下のように投稿の全文が表示されず、毎回 DocBase を見に行く手間が発生するので今回は独自で作りました。

f:id:ba068082:20201126152042p:plain

help.docbase.io

🛠 実装方法

サクッと作りたかったので、DocBase からの Webhook を受ける用の AWS Lambda を作り、レスポンスを Slack 投稿用に整形したうえで Slack の Incomming Webhook に投げる方式としました。
構成はこんなイメージです。

f:id:ba068082:20201201083416p:plain

次項から簡単に実装を説明します。

1. Slack Appの作成

最初に Slack API から Slack App を作成して Incoming Webhooks を設定をします。

https://api.slack.com/apps

f:id:ba068082:20201126151540p:plain

ここで発行した Incoming Webhook の URL を次項で作る Lambda の送信用URLとして使います。

2. Serverless Framework で Webhook 応答用の Lambda を作成

AWS の Lambda の実装です。
Lambda は Serverless Framework を使って開発しました。
Serverless Framework は Lambda や cloud functions の開発環境構築・デプロイを楽に実現するためのライブラリです。

www.serverless.com

今回は以下コマンドで、TypeScript の Lambda 環境を作りました。

$ npx sls create -t aws-nodejs-typescript -n docbase-nippou-notifier -p docbase-nippou-notifier

これだけで指定のパスに Lambda の関連コードが生成されます。
あとは生成されたディレクトリに移動してコードを編集します。

今回は以下のような関数を作りました。
DocBase の Webhook からの POST リクエストを受け取り、Slack の Incoming Webhook に投げるだけです。

import { APIGatewayProxyHandler } from 'aws-lambda';
import 'source-map-support/register';
import { DocBaseWebhookPayload } from "./lib/types";
import { createPostBlock } from "./lib/createPostBlock";
import { createCommentBlock } from "./lib/createCommentBlock";
import fetch from 'node-fetch';

// DocBaseからのイベントを受け取り、整形したデータをSlackにPOSTする
export const webhook: APIGatewayProxyHandler = async (event, _context) => {
  try {
    const payload = JSON.parse(event.body) as DocBaseWebhookPayload;

    if (isTargetTeamPost(payload)) {
      await postToSlack(payload);
    }
  } catch (error) {
    console.error(error);
  }

  return {
    statusCode: 200,
    body: JSON.stringify({
      message: 'ok',
    }, null, 2),
  };
}

// 対象チームの投稿かどうかの判定
const isTargetTeamPost = (payload: DocBaseWebhookPayload) => {
  return payload.post.tags.some((t) => t.name === process.env.TEAM_TAG_NAME);
}

// Slack Incoming WebhookへのPOST
const postToSlack = async (payload: DocBaseWebhookPayload) => {
  const messageBody =
      "comment" in payload
          ? createCommentBlock(payload)
          : createPostBlock(payload);

  const options = {
    method: "POST",
    headers: { "Content-type": "application/json" },
    body: JSON.stringify(messageBody),
  };
  await fetch(process.env.SLACK_WEBHOOK_URL, options)
}

Slack への投稿の整形は以下で行っています。
レイアウト指定の JSON は、Slack Block Kit Builder を使って構築するのがおすすめです。

import { DocBaseWebhookPayload } from "./types";

export function createPostBlock(payload: DocBaseWebhookPayload) {
  const post = payload.post;

  return {
    blocks: [
      {
        type: "header",
        text: {
          type: "plain_text",
          text: `:pencil2: 日報 posted by ${post.user.name}`,
        },
      },
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `<${post.url}|${post.title}>`,
        },
      },
      {
        type: "divider",
      },
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: post.body,
        },
        accessory: {
          type: "image",
          image_url: post.user.profile_image_url,
          alt_text: "user thumbnail",
        },
      },
      {
        type: "divider",
      },
    ],
  };
}

あとは、serverless.tsを編集して Lambda、API Gateway、環境変数、デプロイ先を設定して以下コマンドを実行すれば指定の Lambda にデプロイされ、API Gatewayまで作成されます。

$ npx sls deploy

Serverless Framework 便利!

3. DocBase の Webhook 設定

最後に DocBase の Webhook の設定を行います。 以下記事の通りに設定すればOKです。設定するURLは Serverless Framework でデプロイした Lambda のエンドポイントとなります。 これで完成です 🎉

help.docbase.io

終わりに

Lambda はちょっとした Bot を作るのにも使いやすいですね。
Misoca の slack には他にも Lambda で作られた Bot が多数生息しています。

tech.misoca.jp

今後も業務・個人で色々作っていきたいなと思いました。

🎺 宣伝

Misoca 開発チームでは日常の些細なことでも技術で解決していくエンジニアを募集しています。

Thinreports に SectionReport フォーマット機能を追加する pull request を作成した

こんにちは、Misoca開発チームの日高(@hidakatsuya)です。

以前、Thinreports の SectionReport フォーマット機能を公開したという記事を書きました。

tech.misoca.jp

この度、この機能を pull request 及び issueとして、Thinreports コミュニティに提案しましたので、その内容について紹介します。また、SectionReport フォーマットでは何ができるのかを知ってもらうために、いくつかの特徴もご紹介します。

SectionReport フォーマット機能のコミュニティへの提案

提案の概要

github.com

機能の概要や使い方、SectionReport フォーマット機能の今後の開発方針などを説明しています。SectionReport フォーマットという機能がどういうもので、どのような背景があり、どのような方針で実装されているかなど、この issue を読むことでそれらを把握することができます。

内容は概ね次の通りです。

  • コミュニティで提案されている仕様 をベースに SectionReport フォーマットを実装した
  • 未実装の仕様や既知の課題が残っているが、それらについてはコミュニティと一緒に開発していきたい
  • SectionReport フォーマットの仕様の説明として、いくつかのサンプルコード (Rubyコード、テンプレート、出力結果PDF) を作成したので参考にして欲しい
  • SectionReport フォーマットの使い方として Hello World を説明
  • 私たちが実装した SectionReport フォーマットは未完成で問題もあるが、今後はコミュニティと一緒に開発を進めたい。その提案として、一旦現状で取り込み、仕様の議論とタスクの整備を行って、広く開発に参加できる環境を作って進めてはどうだろう

実装の詳細 (pull request)

Thinreports の Generator (rubygem) と Editor (テンプレートデザイナ) の実装は、それぞれの pull request として作成しています。

github.com

github.com

pull request の説明では、使い方や実装ステータス (未実装、独自実装、既知の問題) について詳しく記載しています。

ぜひご意見ください

私たちが提案した SectionReport フォーマットは、未完成でいくつかの問題もあります。SectionReport フォーマットの機能としての仕様はもちろん、今後の開発の進め方など、広くディスカッションした上で、より良い形で開発を進めたいと考えています。

issue の内容への質問や意見、実際に動かしてみた感想や質問、コードに対する指摘などなど、ぜひご意見をいただけると嬉しいです。

SectionReport フォーマットで何ができるのか

現行の Thinreports のフォーマット (以降、現行のフォーマットと呼ぶ) との比較として、SectionReport フォーマットの特徴を二つ紹介します。

そもそも、SectionReport フォーマットって何?という方は、まず Thinreports コミュニティで提案されている仕様 をご覧ください。

ヘッダーやフッター、明細行を任意の数だけ定義し、組み合わせて出力することができる

現行のフォーマットでは、ヘッダーやフッターといった概念自体がありません。近い機能として「リスト」というツールがありますが、ヘッダーやフッター、明細行は一つだけしか定義することができません。

SectionReport フォーマットでは、次のように、いずれも任意の数だけ定義することができ、名前(ID)によって、それらを組み合わせることができます。

例えば、次のようなテンプレートを用意します。

f:id:hidakatsuya:20201012180259p:plain
テンプレート (Thinreports Editor)

次のコードで PDF を生成します。

report_params = {
  type: :section,
  layout_file: 'example1.tlf',
  params: {
    groups: [
      {
        headers: {
          header1: { items: { text_block1: 'タイトル' } },
          header2: { display: false }
        },
        details: [
          { id: :detail_a, items: { text_block1: '明細1行目' } },
          { id: :detail_a, items: { text_block1: '明細2行目' } },
          { id: :detail_b, items: { text_block1: '明細3行目' } },
          { id: :detail_b, items: { text_block1: '明細4行目' } },
          { id: :detail_a, items: { text_block1: '明細5行目' } }
        ],
        footers: {
          footer1: { display: false }
        }
      }
    ]
  }
}

File.binwrite('example1.pdf', Thinreports.generate(report_params))

出力される PDF は次のようになります。

f:id:hidakatsuya:20201012174550p:plain
出力結果PDF

パラメータによって、テンプレートに定義されたヘッダーを非表示にしたり (display: false)、複数の明細の定義を組み合わせて出力するといったことが可能です。

自動的に領域の高さが伸縮する

現行のフォーマットは、設定した用紙をキャンバスとして、テキストや図形などを配置してレイアウトを作成します。そのため、出力する PDF の高さも用紙の高さで固定され、テキストや図形などの描画位置やサイズも固定です。そのため、テキストの内容によって、図形の位置を下にずらしたり、領域の高さを動的に変更することが困難です。

SectionReport フォーマットでは、ヘッダーやフッター、明細行の高さの動的伸縮をサポートしています。また、動的伸縮によって、図形などの描画位置も動的に追従することが可能です。次の例をご覧ください。

次のようなテンプレートを用意します。

f:id:hidakatsuya:20201012175515p:plain
テンプレート (Thinreports Editor)

次のコードで PDF を生成します。

report_params = {
  type: :section,
  layout_file: 'example2.tlf',
  params: {
    groups: [
      {
        headers: {
          header1: { items: { title: '長いタイトル ' * 15 } },
        },
        details: [
          { id: :detail_a, items: { detail_text: '短い明細' } },
          { id: :detail_a, items: { detail_text: '長い明細 ' * 18 } },
          { id: :detail_a, items: { detail_text: '短い明細' } }
        ]
      }
    ]
  }
}
File.binwrite('example2.pdf', Thinreports.generate(report_params))

出力される結果は次のようになります。

f:id:hidakatsuya:20201012175703p:plain
出力結果PDF

定義した領域を超えるテキストをセットした場合でも、ヘッダー1 の領域の高さが自動的に拡張し、ヘッダー2 や明細A がその下に続いて正しく描画されます。また、明細Aの二行目では、拡張した領域に合わせて四角形の高さが拡張していることがわかると思います。

最後に

興味のある方はぜひ SectionReport フォーマットで遊んでみてください。

宣伝

Misoca 開発チームでは、積極的に OSS に貢献していきたいエンジニアを募集しています。

www.wantedly.com

Railsの複数DB機能で負荷を分散する

こんにちは。弥生で Misoca を開発している小坂と申します。インターネットには kosappi という名前で存在しています。

前回ご紹介した みんなのコンピュータサイエンス は読んでいただけたでしょうか?

9月末で事業年度が終わる会社は多いかと思います。みなさんは無事に10月を迎えることはできましたか?私は有給休暇の日数が付与されて、とても良い気分です 🏝

今回は、Rails の複数 DB 機能を利用して9月末の高負荷を乗り切った話を紹介いたします。

🔥 月末の高負荷

Misoca は請求書作成ソフトということもあり、月末にアクセスが増加します。

ユーザの増加や、機能が充実したことにより、DB への負荷も増加しています。8月末の負荷は DB の限界に近い値でした。 特に、文書の一覧や検索などの参照系のクエリの比重が高く、機能の充実によってクエリ自体も重いものになっており、問題になっています。

9月末は事業年度が終わるユーザも多く、8月末よりも負荷が高くなり、このままでは DB の処理能力の限界を超えてしまう事態が予想されました。

DB への負荷を改善する方法としては、下記のようなものがあると思います。

  • 重いクエリを見直して軽くする(SQLの改善)
  • DB インスタンスの強化(スケールアップ)
  • DB インスタンスを増やして負荷を分散させる(スケールアウト)

今回は参照系のクエリが問題になっていることもあるので、読み込み専用の DB インスタンス(リードレプリカ)を増やして、一部の重いクエリをそちらに向けることにしました。

🛠 Active Record による複数の DB 利用

Misoca は Ruby on Rails の上で開発されています。

Rails は 6 にバージョンアップした際に、複数の DB 利用をサポートしました。Rails 5 でも、追加の gem を使えば実現できたのですが、Rails 6 からは標準機能として提供されます。

参考 : Active Record で複数のデータベース利用

今回はこの機能を利用して、一部のクエリをリードレプリカに向けました。

一部のクエリをリードレプリカに向ける

まず DB を追加します。

事前に AWS の RDS で、普段使っているインスタンスをレプリケーションするインスタンスを作りました。 レプリケーションの遅延が発生しますが、これは 1ms 以内と示されていたので、今回は問題にならないと判断しています。

メインのDBを primary、リードレプリカを replica という名前にしています。 Rails が DB を判断するために、レプリカとして利用する DB には replica: true を書く必要があります。

production:
  primary: &primary
    <<: *default
    host: <%= ENV['PRIMARY_HOST']  %>
  replica:
    <<: *primary
    host: <%= ENV['REPLICA_HOST']  %>
    replica: true

そして全てのモデルで上記の DB が利用できるようにします。 今回はロールが writing の場合は primary を、 reading の場合は replica をそれぞれ利用します。

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to database: { writing: :primary, reading: :replica }
end

最後に、向き先を変えたいクエリを実行している箇所を、下記のようにブロックに含めればOKです。このブロックの外では必ず role: :writing でクエリが実行されます。 role 以外にも database を直接指定することもできますが、こちらは非推奨となっているので気をつけてください。

ActiveRecord::Base.connected_to(role: :reading) do
    # リードレプリカに向けたいクエリを実行するコード
end

今回は手動でロールを変更しましたが、自動的に切り替えることも可能です。 書き込むクエリの場合は primary、読み込みクエリの場合は replica を使う、といった振り分けを書くことができます。

参考 : Active Record で複数のデータベース利用#コネクションの自動切り替えを有効にする

📉 結果

以上の対策を施した上で、無事に9月末を乗り越えることができました。

DB の負荷はどうなっていたんでしょうか?

primary DB の IOPS 1 を見てみましょう。オレンジが READ、ブルーが WRITE の IOPS です。

リードレプリカの利用開始時期(9/15ごろ)から、READ の IOPS が下がっていることがわかります。 8月末と9月末を比較すると、かなり負荷を減らすことができました。

edited.png (178.5 kB)

今回は9月末の高負荷を、Rails の複数 DB 機能で乗り切ることができました。 今後は、2台に増えた DB のスペックを見直したり、他のクエリをリードレプリカに向けることを検討する予定です。

🎺 宣伝

Misoca 開発チームでは、Rails の新機能を使って課題を解決したいエンジニアを募集しています。


  1. Input/Output Per Second の略です。1秒あたりの入出力の多さを示しています。