Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

トピックモデルを使って問い合わせ内容を分析した話

この記事はClassi developers Advent Calendar 2021の18日目の記事です。
昨日は基盤インフラチームのめるさんによる「バックエンドエンジニアが基盤インフラチームに異動して半年ほど経った話」でした。

こんにちは、データAI部でデータサイエンティストをしている高木です。
弊社では顧客である先生、生徒、保護者からClassiの機能や契約に関する問い合わせを日々頂いております。
これらの問い合わせの内容を分析し、Classiの現状の課題や今後解決していくための施策などを社内で検討しています。

今回は問い合わせ内容を言語処理技術の一つであるトピックモデルを使って分析した内容についてご紹介します。

なぜ分析する必要があったのか?

Classiへの問い合わせやその対応の内容は、担当者によってテキスト化された状態で管理されています。
弊社のカスタマーサポート・カスタマーサクセスチームは、これらの内容を基に次年度の施策を検討したり、実施した施策がどうだったかという効果を分析しています。

年度が変わる3月〜4月での問い合わせが増加傾向にあり、その中でも学校全体の利用に関わる設定・登録の問い合わせが最も多くなっています。
毎年、これらの問い合わせを目視でチェック・分類し、施策を検討している状態だったのですが、作業には膨大な時間と労力がかかってしまいます。

そこで、ある程度自動的に分析・分類できないかという必要性が出てきており、今回トピックモデルを使った分析を試みました。

トピックモデルとは?なぜ使ったのか?

トピックモデルは、文書集合をそれらに含まれる単語の共起性から、どんなトピック(話題や分野など、大体の「意味」のようなもの)が存在するのか、各文書はどのトピックについてのものか、を自動で推定することができます。

これまで様々な手法が提案されており、文書の検索や分類、また、音楽の歌詞探索*1やゲームのデッキアーキタイプの抽出*2など、様々な分野へ応用されています。
私もテスト問題に応用した研究をしています*3

以下は代表的なトピックモデルです。

トピックモデル 特徴 参考文献 参考コード
LSI: Latent Semantic Indexing ・特異値分解による文章の圧縮
・1文書に1トピック
Deerwester et al (1990)*4 Gensim
PLSI: Probabilistic Latent Semantic Indexing ・LSIの確率モデル化
・1文書に複数トピック
Hofmann (1999)*5 PyPI
LDA: Latent Dirichlet Allocation ・PLSIのベイズ化
・新規文書のトピック推定が可能
Blei et al (2003)*6 Gensim
HDP: Hierechical Dirichlet Process ・LDAのノンパラメトリック化
・トピック数が自動決定可能
Teh et al (2006)*7 Gensim
DTM: Dynamic Topic Model ・時系列データへの適用 Blei et al (2006)*8 Gensim
BTM: Biterm Topic Model ・短い文章への適用 Yan et al (2013)*9 GitHub

一方で、問い合わせの内容にはClassiの機能や契約に関する複数の質問が同時に含まれることがあります。
そして、その内容は時期によっても変化します。

そこで今回は、(1)自動で手軽に内容を把握できる、(2)同一の問い合わせから複数のトピックを抽出することができる、(3)内容の時系列変化を追うことができる、 という点を考慮しDTM(Dynamic Topic Model)を使用しました。

下図はDTMによるトピックの推定と時系列変化の例です。
時刻tにおける、ある文書集合のトピックに出現する単語(横軸)と、それらの単語がトピックに出現する確率(縦軸)を表しています。
また、各文書でそれぞれのトピックが出現する確率も推定されます。

f:id:ttakagi1021:20211216200222p:plain
DTMによるトピックの推定と時系列変化のイメージ

DTMによる問い合わせ内容の分析

概要

2020年と2021年の3月〜4月に寄せられた設定・登録についての問い合わせをトピック分析し、内容の変化を考察しました。
DTMを使ったトピック分析の手順と目的は以下になります。
なお、すべての分析はPythonで行いました。

  1. DTMによるトピックの推定
    • 2020年と2021年のそれぞれの問い合わせのトピックとその時系列変化を推定するため
  2. トピックへのラベル付け
    • トピックの内容を把握しやすくするため
  3. トピックごとの問い合わせ件数を集計
    • どのトピックの問い合わせが多いか把握するため
  4. 2020年と2021年のトピック間類似度の計算
    • 類似度の低いトピック(各年の特有なトピック)を抽出するため
  5. ダッシュボードの作成
    • 手順1〜4の結果を可視化するため

分析手順の詳細

1. DTMによるトピックの推定

DTMによるトピックの推定は、Gensimのmodels.ldaseqmodelというライブラリを使用しました。
トピックの推定で使用するメソッドと入力パラメータは以下になります。

dtm_model = LdaSeqModel(corpus, id2word, time_slice, num_topics)
  • corpus:コーパス
    • 文書毎の単語IDとその出現回数を持つタプルリスト
  • id2word:辞書
    • 単語とそれを一意に識別するIDの辞書
  • time_slice:タイムスライス
    • 指定した「期間」ごとの文書数のリスト
  • num_topics:トピック数
    • トピックの数は可変

辞書やコーパスの詳細については省略しますが、これらを作成する際に重要となる単語の抽出処理について後述します。

タイムスライスとは、トピックの変化を指定した「期間」で分割するためのもので、今回はその「期間」を月曜日〜日曜日の1週間ごとにしました。
週毎の問い合わせ件数が以下の場合、time_slice = [3, 5, 1]となります。

  • 2021/03/01(月)〜03/07(日):3件
  • 2021/03/08(月)〜03/14(日):5件
  • 2021/03/15(月)〜03/21(日):1件

さらに、トピック数は分析や分類したい粒度によって手動で設定する必要があります。
今回は以下の理由で両年ともトピック数を10としました。

  • 2020年の問い合わせを調査した際、約10個の重要なカテゴリに分類された
  • 多すぎるとトピックの意味が細かく分散しチェックするのが大変

単語の抽出処理について

トピックモデルではトピックの推定精度を向上させるために、関連のある単語の共起性を高めることが重要になってきます。
一方で、日本語には次のような性質があります。

単名詞Nが対象分野の重要な概念を表しているなら、書き手はNを頻繁に単独で使うのみならず、新規な概念を表す表現としてNを含む複合名詞を作りだすことも多い。*10

この「単名詞N」と「単名詞Nを含む複合名詞」は関連する概念を表していることが多いです。

例えば、Classiのコミュニケーション機能である「校内グループ」では、「グループ」という単語を中心に、「自動作成グループ」、「任意作成グループ」などの機能が存在し、同一の問い合わせ内で出現することが多いです。

この例の場合、「単名詞N」と「単名詞Nを含む複合名詞」は以下になります。

  • 単名詞:校内、グループ、自動、作成、任意
  • 複合名詞:校内グループ、自動作成グループ、任意作成グループ

これらの単語はすべて関連しており、同一のトピックに分類されることが望まれるため、このような単名詞複合名詞をすべて抽出するようにしました。
抽出処理には、形態素解析器のMeCabと形態素解析器の結果を基に複合名詞を抽出することが可能なTermExtractを使用しています。

2. DTMによるトピックの推定

手順1で作成されたdtm_modelにより、特定の期間のトピックに出現する単語と出現確率をリストで出力することができます。
そのメソッドと入力パラメータは以下になります。

dtm_model.print_topic(topic, time, top_terms)
  • topic
    • トピックのID(トピック数10の場合0~9)
  • time
    • タイムスライスで設定した「期間」の順番
  • top_terms
    • 出力する単語の数

このメソッドによる出力結果を用いて、トピックをワードクラウドで可視化しました。
そして、比較的大きく表された単語を基にラベルを付与しました。

最初の週(time=0)のトピックのワードクラウドは以下になります。

f:id:ttakagi1021:20211216184956p:plain
2020年3月 第1週のワードクラウド

f:id:ttakagi1021:20211216185035p:plain
2021年3月第1週のワードクラウド

3. トピックごとの問い合わせ件数集計

dtm_modelでは、問い合わせごとにトピックの出現確率も出力することができます。
そのメソッドと入力パラメータは以下になります。

dtm_model.doc_topics(doc_number)
  • doc_number
    • コーパスや辞書を作成する際に入力した文書の順番

問い合わせごとに最も出現確率の高いトピックをその問い合わせにおけるトピックとし、トピックごとの問い合わせ件数を集計しました。
以下の表は件数の多い順にトピックを並べた結果になります。

2020年3月〜4月

トピックNo ラベル
2 年度更新の各ステップ作業
10 生徒情報の登録
1 生徒や先生の情報登録, ExcelファイルのUPL/DL
6 IDとPW, ログイン, 招待コード, 無償提供
4 管理責任者, 管理者の権限, 生徒カルテ
3 校内グループの設定や配信, 授業の登録
7 学習動画, 学習記録, ダミー生徒
9 継続, 利用停止, 保護者
5 識別番号, 留年や進学した生徒の登録, 人数確定
8 模試データ連携

2021年3月〜4月

トピックNo ラベル
2 生徒情報の登録, ExcelファイルのUPL/DL
3 年度更新
5 年度更新
4 IDとPW, ログイン, 招待コード
8 模試データ連携
7 利用停止, 学習動画パック
6 校内グループ, アンケート
1 管理責任者, 学習動画, 学習記録, 閲覧権限
10 授業登録, 先生登録, 帳票登録
9 人数確定

4. トピック間類似度の計算

手順2で使用したdtm_model.print_topicを用いて、トピックに含まれる単語とその出現確率を50個出力し、2020年と2021年のトピック間の類似度をコサイン類似度で計算しました。

下図はトピック間の類似度をヒートマップによって可視化した結果です。
行が2020年、列が2021年のトピックを示しており、それぞれの類似度の値と値が大きいほど色が濃く表示されています。

例えば、2021年のtopic9は同年の他トピックに比べ2020年のトピックとの類似度がすべて0.5未満と低くなっています。
このことから、topic9は2021年特有なトピックだということが分かります。

f:id:ttakagi1021:20211216190648p:plain
2020年(行)と2021年(列)のトピック間類似度のヒートマップ

5. ダッシュボードによる可視化

手順1〜4の結果をTableauのダッシュボードで可視化しました。

下図は2021年のダッシュボードです。
このダッシュボードで分かることは以下になります。

  • 各年度のトピックを表す単語やその出現確率(図中①)
  • トピックごとの単語の週次変化(図中②)
  • 各トピックに該当する問い合わせ内容(図中③)
  • トピックごとの問い合わせ件数(図中④)
  • トピック間の類似度(手順4のヒートマップを別ページで作成)

f:id:ttakagi1021:20211216201758p:plain
2021年3月〜4月のトピック分析ダッシュボード

内容変化の考察

以上の分析結果を基に、2020年と2021年の内容の変化を考察しました。
ページの都合上詳細は省きますが、いくつか興味深い結果を以下で紹介します。

「年度更新」における各ステップの問い合わせの減少

手順3の問い合わせ件数の多いトピックを見ると、両年ともに次年度のユーザー情報を更新・登録する「年度更新」や「情報登録」についてのトピックが上位3位までに入っています。

2020年はこれらのトピックで「ステップ」や「STEP」という単語を含む問い合わせが多かったのですが、2021年ではそれらの単語を含む問い合わせが減少していました。

この理由としては、2020年から年度更新の手順をステップごとに説明したガイドが作成されたため、ステップごとに問い合わせをする人が増えたと考えられます。

そして、2021年はこれらのガイドを基にした作業を初めて行う先生の数が減少した(作業を理解している先生が増加した)ため、これらの単語を含む問い合わせが減少したと考えられます。

下図は2020年3月〜4月の「年度更新」に関するトピックの推移です。

f:id:ttakagi1021:20211216201916p:plain
2020年の「年度更新」に関するトピックの推移

「利用生徒人数の登録確定」についての問い合わせの増加

手順4で述べたように、2021年のtopic9は同年の他トピックに比べ2020年のトピックとの類似度が低くなっており、2021年特有なトピックとなっています。

このトピックは利用生徒人数の登録確定に関するもので、その作業方法や締切についての問い合わせが多かったです。

この理由としては、2021年は4月に入ってからこれらの確定作業を完了するよう各学校への連絡が増えていました。
その結果、これらの確定作業に関連する問い合わせが増えたと考えられます。

下図は2021年3月〜4月の利用生徒人数の登録確定に関するトピックの推移になります。
4月以降でこのトピックに関連する単語の出現確率が上昇傾向になっているのが分かります。

f:id:ttakagi1021:20211216202022p:plain
2021年の「利用生徒人数の登録確定」に関するトピックの推移

最後に

今回の分析結果から、現状のClassiの設定・登録に関する課題や施策の効果などを考察することができました。
この結果をチームを超えて見ることによって、今年の施策はどうだったのか、今後どういう施策を打っていくべきかを考えることができます。

例えば、カスタマーサポートチームでは今回の分析で、生徒人数の登録確定や年度更新において各設定の期日が分かりづらいことや、年度更新のSTEP3の問い合わせが多いことが明らかになったため、これらの問い合わせを10%減らすことを目標に改善活動を進めています。

また、PMM(プロダクトマーケティングマネージャー)チームでは顧客課題をプロダクトへ反映するために、今回の分析手順をユーザーから寄せられる機能改善についての要望に応用し、その内容の深堀りや要望を実現するための開発の優先順位付けを検討しています。

このように、今回の取り組みが各チームで進められている施策に少しでも寄与できていることは、データAI部としても嬉しい限りです。
分析結果を毎回真摯に聞いて下さりご意見下さる各チームの皆様にはとても感謝しています。

現在、10月までの問い合わせ内容の分析が完了しており、今後は来年の2月までの問い合わせ内容の分析を実施し、年間の問い合わせの傾向を整理していく予定です。

さらに、来年度以降の問い合わせ内容の継続的な分析や、それらの結果を各チームへ素早く還元できる仕組みを構築していきたいと考えています。

明日のClassi developers Advent Calendar 2021の担当は横田さんです。
よろしくお願いします。

*1:Lyric Jumper: https://lyric-jumper.petitlyrics.com

*2:『逆転オセロニア 』における、機械学習モデルを用いたデッキのアーキタイプ抽出とゲーム運用への活用: https://www.slideshare.net/RyoAdachi/deck-archetype-extraction-cedec2019

*3:高木輝彦,高木正則,勅使河原可海,田中健次: e テスティングにおけるLDAを用いた項目間類似度の算出, 情報処理学会論文誌, Vol.55, No.1, pp.91 - 104, 2014.

*4:Deerwester, S., Dumais, S. T., Furnas, G. W., Landauer, T. K. and Harshman, R.: Indexing by Latent Semantic Analysis, Vol. 41, No. 6, pp. 391–407 (1990).

*5:Hofmann, T.: Probabilistic latent semantic indexing, Proceedings of the 22nd annual international ACM SIGIR conference on Research and development in information retrieval, SIGIR ’99, New York, NY, USA, pp. 50–57 (1999).

*6:Blei, D. M., Ng, A. Y. and Jordan, M. I.: Latent dirichlet allocation, J. Mach. Learn. Res., Vol. 3, pp. 993–1022 (2003).

*7:Teh, Y. W., Jordan, M. I., Beal, M. J. and Blei, D. M.: Hierarchical Dirichlet Processes, Journal of the American Statistical Association, Vol. 101, pp. 1566–1581(2006).

*8:Blei D. M. and Lafferty J. D.: Dynamic topic models, Proceedings of the 23rd international conference on Machine learning, pp. 113-120 (2006).

*9:Yan, X., Guo, J., Lan, Y. and Cheng, X.: A biterm topic model for short texts, Proceedings of the 22nd international conference on World Wide Web, pp. 1445–1456 (2013).

*10:中川裕志,湯本紘彰,森 辰則:出現頻度と連接頻度に基づく専門用語抽出,自然言語処理,Vol. 10, No. 1, pp. 27–45 (2003).

UIKitでDesign Systemを実装する

この記事はClassi developers Advent Calendar 2021 の 14日目の記事です。

はじめまして。小中事業開発部でモバイルアプリエンジニアをしています拜郷です。
今回は新規開発中サービスのiOSアプリでDesign System1を実装するにあたって考えたことを書いていきます。

Design System導入の背景

小中事業開発部では現在新規サービス tetoru(テトル) のリリースに向けてチーム開発を行なっています。
開発を進めていく中でUXDチームからDesign System導入の検討がされました。
導入の背景としては開発メンバーが増員するタイミングだったのと、デザインツールの移行が決まったのが大きな要因としてありました。
Design Systemはプロダクト、チームそれぞれの視点で以下の目的を実現できると考えています。

  • プロダクトを通してユーザに一貫性のある体験を提供できる
  • チーム内でデザイン原則を共通認識として持つことができる
  • メンテナビリティとスケーラビリティの向上

iOSアプリ開発の背景

iOSアプリは開発初期の段階からUIKitおよびInterface Builder(以下IB)を扱うStoryboard, xibを使用して開発を行なっていました。
Design System導入のためにIBでのレイアウトをやめたりSwiftUIに変更するのは諸々の事情により現実的ではなかったため、前提として開発方針は変えずDesign Systemの実装検討を進めることになりました。
ただしiOSアプリ開発においてUIKitおよびIBはDesign Systemの実装(主にUIコンポーネントの再利用)に適しているとは言えず少々困難です。
以下ではDesign SystemをUIKitで実装するにあたって考えたことを具体例を交えて紹介していきます。
(※今回紹介するソースコードはすべて本記事用のサンプルコードです。)

デザイントークンの実装

まずDesign Systemにおける最小単位であるデザイントークンの実装を考えます。 デザイントークンにはカラー、余白、行間、Elevation(高さ)、タイポグラフィ、シャドウ、アニメーションなど複数のコンポーネントにまたいで使用される情報を定義します。

カラー

カラーの定義はAsset Catalogを使用します。これはXcode9およびiOS11から使える標準機能なので現時点では特に悩まず使えます。
定義したカラーをコード内で使用する場合、UIColorクラスの初期値にAsset Catalogで定義した文字列を指定します。
ここでSwiftGenR.Swiftを使用することでタイプセーフにリソースを扱えるようになり、タイプミスなどで実行時エラーが発生しなくなる等のメリットがあります。
またAsset CatalogのColorはライト/ダークの各モードをそれぞれ定義することができるため、デザイントークンのカラー定義を双方で検討しておくことでダークモード対応が容易に行えると思います。

// 通常コードからカラーを取得する
UIColor(named: "primary")
// SwiftGenを使用する
Asset.Color.primary.color
// R.Swiftを使用する
R.color.primary()

タイポグラフィ

タイポグラフィの定義はUIFontをextensionして使用することにしました。
SystemFontを使用する場合は単純にトークン名に紐づいたサイズとウェイトを指定して定義します。
カスタムフォントを使用する場合はサイズ、ウェイトに加えフォントファミリーを指定します。ここでもSwiftGenやR.Swiftを使用することでタイプセーフにリソースを扱えます。

public extension UIFont {
    static let title: UIFont = .systemFont(ofSize: 44.0, weight: .bold)
    static let body: UIFont = .systemFont(ofSize: 14.0, weight: .bold)
    static let caption: UIFont = .italicSystemFont(ofSize: 11.0)
    static let button: UIFont = .systemFont(ofSize: 14.0, weight: .regular)
    static let swiftGenSample: UIFont = FontFamily.mplus1.regular.font(size: 14.0)  // SwiftGenでカスタムフォントを指定する
    static let rswiftSample: UIFont = R.font.mplus1pRegular(size: 14.0)!            // R.Swiftでカスタムフォントを指定する
    ...
}

レイアウト要素

余白、角丸、高さなどレイアウト要素の数値を扱うデザイントークンはCGFloatをextensionして定義するようにしました。

public extension CGFloat {
    struct spacing {
        static let xxx_small: CGFloat = 2
        static let xx_small: CGFloat = 4
        static let x_small: CGFloat = 8
        ...
    }

    struct cornerRadius {
        static let small: CGFloat = 2.0
        static let medium: CGFloat = 4.0
        static let large: CGFloat = 8.0
        ...
    }
}

ただしIBからここで定義したレイアウト要素の情報を直接参照できないという課題があります。
この課題に対する1つの解決策として、IB上で設定したNSLayoutConstraintをIBOutletを使ってコードと紐づけるという方法があります。
たとえばある画面をレイアウトする際にSafeAreaとコンポーネントの余白に定義したデザイントークンの数値を使いたい場面があるとします。
この場合IB上では仮の値を設定したNSLayoutConstraintをIBOutletに紐づけます。
あとは紐づけたNSLayoutConstraintにデザイントークンとして定義した値を設定することができます。

f:id:khaigo:20211213101536p:plain
IB上で設定したNSLayoutConstraintをIBOutletでコードと接続する

@IBOutlet weak var btnTrailingConstraints: NSLayoutConstraint! {
    didSet {
        btnTrailingConstraints.constant = <デザイントークンとして定義した値>
    }
}

しかしこの方法はデメリットも存在します。
特に下記のデメリットはIBを使用するメリットを打ち消すので、今回こちらの方法は採用しませんでした。

  • すべての制約をIBOutletに紐づけてコード上で管理するとコード量が肥大化する
  • 動的に制約を書き換えることになるのでStoryboard, xib上のレイアウトと実際に表示されるレイアウトに差異が出る

UIコンポーネントの実装

UIコンポーネントはボタンやフォームなどデザインに使用されるUIパーツを定義します。基本的にはここで定義したコンポーネントの組み合わせで各画面のレイアウトが完成します。
実装方針としてはIBDesignableおよびIBInspectableを使用し、UIKitの各パーツをラップしたクラスを定義しそれをIB上でカスタムクラスとして設定することにしました。
カスタムクラスは定義した各デザイントークンを組み合わせることで共通化を図ります。

またUIコンポーネントを定義する上で、コンポーネントを共通化する単位についてデザイナーとしっかり認識を合わせることが重要になります。
ここでエンジニア、デザイナー間で認識齟齬が起きると以下のような問題が発生するかと思います。

  • 持つべき機能が異なるコンポーネントを共通化してしまう
  • 同じ目的のコンポーネントを別コンポーネントとして切り分けてしまう

いずれもUIコンポーネントが二重管理になったり、想定外の画面でハレーションが起きるなどメンテナビリティ、スケーラビリティを低下させる要因になります。

UILabel

基本方針の通り実装します。カスタムクラスにIBDesignableを付与することでIBから適用したUIが確認できます。
ここでのポイントはUILabelを構成する要素はすべてデザイントークンで定義したものを使用している点です。

@IBDesignable
class TitleLabel: UILabel {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupAttributes()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupAttributes()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        setupAttributes()
    }
    
    private func setupAttributes() {
        // デザイントークンで定義した値を設定する
        font = .title
        textColor = R.color.systemGray_800()
    }
}

@IBDesignable
class BodyLabel: UILabel {
    ...
}

@IBDesignable
class CaptionLabel: UILabel {
    ...
}

定義したUILabelのカスタムクラスを設定することでIB上で反映されたスタイルを確認する事ができます(以下サンプル)。

f:id:khaigo:20211213101832p:plain
定義したUILabelのカスタムクラスを設定する

UIButton

こちらも基本方針通りです。
UIButtonの状態に応じてlayoutSubviewsでスタイルを切り替えるようにしています。
その他UIViewを継承するいずれのUIクラス(UITextView, UITextField etc)もこの方針でカスタムクラスを定義してUIコンポーネントを作成するようにします。

@IBDesignable
class FillButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupAttributes()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupAttributes()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        setupAttributes()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        switch state {
        case .normal:
            backgroundColor = R.color.brandAccessible()
            titleLabel?.textColor = R.color.systemGray_000()
        case .highlighted:
            backgroundColor = R.color.brandAccessibleActive()
            titleLabel?.textColor = R.color.systemGray_000()
        case .disabled:
            backgroundColor = R.color.buttonDisabled()
            titleLabel?.textColor = R.color.disabled()
        default:
            break
        }
    }
    
    private func setupAttributes() {
        layer.cornerRadius = .cornerRadius.medium
        backgroundColor = R.color.brandAccessible()
        titleLabel?.font = .button
        titleLabel?.textColor = R.color.systemGray_000()
    }
}

@IBDesignable
class CancelButton: UIButton {
    ...
}

@IBDesignable
class TextButton: UIButton {
    ...
}

定義したUIButtonのカスタムクラスを設定することでIB上で反映されたスタイルを確認する事ができます(以下サンプル)。

f:id:khaigo:20211213102016p:plain
定義したUIButtonのカスタムクラスを設定する

まとめ

最後までお読みいただきありがとうございます。
まだまだ諸々の事情によりSwiftUIではなくUIKitを使用するプロジェクトは多々あるかと思います。
実装に関しては基礎的な内容だったかも知れませんがDesign Systemを実装する際の考え方の参考になると幸いです。
私自身もDesign Systemとその実装についてまだまだ模索中ではありますが今後もUnlearn & Learnでやっていきたいです!

Classi developers Advent Calendar 2021 の 15日目はおかじさんです。


  1. Design Systemとは諸説ありますが、大まかに言うとプロダクトのデザインにおける一貫性を効率的に保つための仕組みのことです。
    Design Systemを構成する要素としては、概念・原則をまとめたドキュメント、スタイルガイド、UIコンポーネントライブラリ、またそれらを管理・運用するためのルールやツールなどがあげられます。

IAM Policy Simulator で「必要な権限足りてる?」を確かめる

この記事は Classi developers Advent Calendar 2021 の13日目の記事です。

こんにちは。開発本部プロダクト開発部学習チームでエンジニアをしています、藤田です。 本記事では AWS の IAM の Policy の定義から、アクセス可能なリソース範囲・許可されるアクション等を事前に検証できる IAM Policy Simulator を紹介します。

続きを読む

EC2からECSへ移行する道のり

開発本部の onigra です。今回の記事は、Classiのアプリケーション実行環境をAmazon EC2からECSに移行しているお話をします。

この記事では「Ruby on RailsのWebアプリケーションをECSに移行する上での技術的なトピック」ではなく、「なぜClassiはEC2からECSに移行する必要があるのか」「どのように移行を進めているのか」についてをお話します。

なお、この記事は Classi developers Advent Calendar 2021 11日目の記事です。昨日は データプラットフォームチームの滑川さんによる Cloud Composer 2へのupgradeでどハマりした話 でした。

なぜEC2からECSに移行する必要があったのか

私が入社した当初、Classiは前述の通りAmazon EC2で稼働していました。それ自体に問題は無いのですが、サーバのプロビジョニングとデプロイが遅く、不安定という課題を抱えていました。

また、普段プロダクトを開発・運用しているチームの中にデプロイ、インフラの知識や、既存の仕組みを把握しているメンバーが少なく、プロビジョニングやデプロイに何か問題があると、私のような組織横断的な技術課題に取り組むメンバーが対応にあたることが多々ありました。

近年、書籍 『LeanとDevOpsの科学』 でも語られているように、変更のリードタイムとデプロイの頻度は組織のパフォーマンスに大きく関わる要素であり、今後価値提供のスピードを上げていくにあたって大きな障害となるため、なんとか全社的に解決したい課題だと考えていました。

そんな中、 2020年の春に不正アクセスが発生し、その後サービスの高負荷によるアクセス障害が続く状態 になりました。

日々対応に追われる中、次から次へとサーバのチューニングやアプリケーションの修正をデプロイしなければならない状況で、デプロイとプロビジョニングの仕組みが大きなボトルネックになっていることをこの時に改めて痛感します。

また、今後Classiがセキュリティ面でもパフォーマンス面でもお客様からの信頼を回復しなければならない中で、今のままの仕組みではとても改善のスピードを上げることはできません。

この時、現状のEC2へのプロビジョニング、デプロイの仕組みを全て捨て、ECSに移行して1から作り直そうという決断をしました。

移行の戦略

最初の移行の成否が今後を左右する

この時点で現場は日々の対応で疲弊していました。そんな状況を打破し、サービスの改善にポジティブな勢いを生むためには、移行する1つめのリポジトリの成果に大きく左右されると考え、確実に成功させる必要がありました。

そのため、最初の移行対象はコンテナ化の難易度が比較的低いWeb APIの機能のみを提供しているリポジトリを選択することにしました。

運用ができるメンバーを増やしつつ、移行する

私はコンテナを普段から利用し、ECSを触った経験があったので、移行する上で知識の問題は無いのですが、コンテナの運用に慣れていないメンバーへレクチャーをしきれるのかという懸念がありました。

また、Classiには多くのリポジトリがあり、それらを1つ1つ移行していたらかなりの期間がかかってしまうことは容易に想像できます。そのため、移行作業と移行のレクチャーができる人員をスケールさせる戦略を練る必要がありました。

そのため、移行作業は以下のようにメンバーを巻き込みつつ、最終的に自分以外のメンバーがレクチャーを行えるような状態を目指して進めることにしました。

  1. 自分で検証環境の移行を行い、必要な作業を把握する
  2. 本番環境の移行に取り掛かる際に一緒に作業するメンバーをアサインし、ペア作業でレクチャーしながら移行を完了させる
  3. 次のリポジトリに取り掛かる際に新たにメンバーをアサインし、2のメンバーにレクチャーしてもらう
  4. 以降、自分はサポート役に回る

また、本番リリース直前は必ず一緒に「あと何をすればリリースできるのか」を確認する機会を設けました。 いわゆるリリーススプリントに臨むための準備です。以下のような内容を確認、レクチャーし、リリースマネジメントを行いました。

  • Datadogの使い方
  • Sentryの使い方
  • 依存するアプリケーションの考慮
  • 本番ネットワーク疎通確認と方法の確認
  • リリースの手段と、ロールバックの仕方
  • 関係各所への連絡と期待値調整
  • EC2環境と、不要になるAWSのリソースを削除する手順

これらを移行作業の中でレクチャーすることによって、今後リポジトリ担当者が自立してサービスを運用できる状態になることを目指しました。移行の完了は終わりではありません、始まりなのです。

結果

2021年12月現在、13のリポジトリが移行完了しています。

何が移行を勢いづけたのか

フォロワーシップを強く発揮するメンバーの存在

同じ課題感を感じていたメンバーがフォロワーシップを発揮し、2つめのリポジトリの移行作業を早々に完了させ、以降に続くメンバーの勧誘とレクチャーを積極的に行ってくれました。

そのおかげで予定よりも早く自分がサポーターの役割に移行でき、アプリケーション固有の問題や例外的な対応をメインに行うことによって、移行プロセスのボトルネックの解消にフォーカスすることができました。

成長機会を貪欲に掴みにきた新卒、若手メンバーの存在

移行するメンバーを募集した際、新卒や若手が積極的に手をあげて参加してくれました。私自身、キャリアの中でもトップレベルに困難な状況と感じていたのに、それを成長機会と捉え、できることをしようとする姿勢には強く勇気づけられました。

会社の危機的な状況に対し、若いメンバーからサービス改善に繋がるポジティブなムーブメントを起こせたことは、非常に大きな意義があったと思っています。

移行に参加したメンバーの1人である小川さんがECS化について書いた記事も是非ご覧ください。

新卒1年目でECS化に取り組んだことを振り返る

移行に関わるメンバー同志のコミュニティのようなつながり

私はチャットへのレスポンスが早い方なのですが、移行作業を行うメンバーが集まるチャネルではよりそれを心がけました。未知の作業を行うことに不安を感じているメンバーに対し、何か書き込めばレスポンスが返ってくる安心感を与えたかったからです。結果的にその行動はチャネルが盛り上がることに繋がり、コミュニティのようになりました。

移行セレモニー

社内にポジティブな雰囲気を起こすことと、移行しきったメンバーを称えて成功体験にしてもらうために、移行が完了したら担当したメンバーにオープンチャンネルで移行完了宣言してもらい、スタンプを可能な限りつけて盛大に祝うというセレモニーを必ず行っていました。

f:id:onigra:20211208190426j:plain

その後

運用の知識を身につけたメンバーを触媒に、社内で徐々にサービス運用を改善する動きが浸透していきました。

運用改善を行うWorking Groupの定期開催化

まず、株式会社はてな様で行っている Performance Working Group を自主的に行うチームが現れました。その活動が他のチームに広がり、チームで個別に行うだけではなく、インフラチームが主催で行う組織横断的なPerformance Working Groupも定期開催されるようになりました。

また、定期的にSentryの整理やエラー内容の調査する取り組みも各チームで行われています。アプリケーションやエラー監視ツールはインシデント以前から導入されていたものの、これまではエラーが多すぎたり、使い方がわからないメンバーも多く、放置されがちだった状況から大きく改善されました。

これは、lacolacoさんがSentryについての継続的な活用支援を続けてきてくれたことも大きな後押しであったと感じています。

Sentryを活用するためにやっていること

システムアラート発生時のメンバーのレスポンスが向上した

インシデント以前はシステムアラートの発生に対して、特定のメンバーしか反応しない・できない状態で、反応したメンバーが担当しているチームに「これ大丈夫ですか?」と伝えに行くことも多かったのですが、徐々に反応するメンバーが増えてきて、今では担当チームのメンバーがシステムアラート発生から真っ先に「確認します」と反応することがほとんどになりました。

これにはインフラチームが主体で行っていた、サービスのピークタイムの監視を当番制で行う取り組みが定常化し、インフラチーム以外のメンバーもその当番に参加するようになり、システムアラートへの対応経験を積んだメンバーが徐々に増えていったことも後押しになりました。

今となってはピークタイムの監視当番は、システムアラート対応のオンボーディングの役割も兼ねるようになりました。

インフラに興味を持つメンバーが増えた

移行作業をきっかけにインフラに興味を持ち、自主的にAWSの資格を受験して合格したメンバーも数名います。

AWS 認定 ソリューションアーキテクト - アソシエイト(SAA-C02) 試験に合格したので振り返る

受験費用は会社でサポートしているのですが、最初に受験したメンバーは会社で精算できるということを知らず、たまたまSlackで「この間の試験受かってよかった〜」という発言を見かけて「会社で精算できるから申請してね」と声をかけたこともありました。

また、理解の必要はあるが自分で構築することは少ないAWSのネットワーク(VPC、Subnetなど)について、移行に参加したメンバーが勉強会を開催したいと相談され、一緒に内容を考えてハンズオンを実施したりもしました。

その他

監視についての記事を開発者ブログに執筆してくれるメンバーもいました。

AWS ECS監視のオオカミ少年化を防ぐために考えたちょっとしたこと Amazon EventBridge(CloudWatch Events)で動かしているバッチをDatadogで監視する仕組みを構築した話

現状維持を良しとせず、試行錯誤しながらアウトプットする姿勢はとても頼もしく感じます。

反省点

ここまで良かった点ばかり書いていますが、当然省みることもあります。

「移行に関わりたい」というメンバーが想定以上に多く、移行作業が勢いよく進んでしまい、私がコントロール仕切れないこともありました。また、私が移行作業しつつアクセス障害への対応も行なっていたため、当然作業の中でボトルネックになってしまうことも発生し、十分にサポート仕切れないまま進んでしまい、不安を感じさせてしまったメンバーもいました。私がボトルネックになってしまうことは、事前に予測できていたことでした。

移行のふりかえりを行なった際に、「表面的にうまくいってると評価されているECS移行のイメージと参加当初ギャップがあった」というフィードバックをもらいました。これは当時、十分なサポート体制を組めなかった状態を的確に指摘していると感じています。

最後に

結果、Classiでは現在ほとんどのサービスをECSで運用し、デプロイの安定化と頻度の向上に成功しました。また、移行作業を通して若手メンバーを中心に、全社的な運用力の底上げに繋げられたと感じています。最悪の状況から前を向けるようになったのは、間違いなくこの時に成長してくれたメンバーのおかげです。

一方で繰り返しになりますが、移行の完了は終わりではありません、始まりなのです。移行は価値提供のスピードを上げていくにあたっての最低条件でしかありません。また、「ほぼ」と書いた通り移行完了していないリポジトリは残っています。

引き続き、より良いサービスを運用していくために精進していきます。

Cloud Composer 2へのupgradeでどハマりした話

この記事は Classi developers Advent Calendar 2021 の10日目の記事です。

こんにちは、データプラットフォームチームの滑川(@tomoyanamekawa)です。
Google CloudのCloud Composerのversion2(Cloud Composer 2)がpreview公開され、Terraformでも10月末から作成可能になりました*1

「Cloud Composer 2ならworker数をautoscalingしてくれるらしい。そんなに設定変わらないだろうからサクッと移行しよう。」 くらいの軽い気持ちでCloud Composer 1からupgradeをしましたが、てこずってだいぶ時間を溶かしてしまいました。

そのハマったポイント4つとCloud Composer 2へupgrade完了した上での所感をまとめた記事です。

※追記:
2021年12月16日にgenerally available (GA)になりました。 https://cloud.google.com/composer/docs/release-notes#December_16_2021

*1:v3.90.0からnode_configを含めたCloud Composer 2をTerraformで作成できるようになった

composer: removed config.node_config.zone requirement on google_composer_environment (#10353) https://github.com/hashicorp/terraform-provider-google/releases/tag/v3.90.0

続きを読む

Amazon EventBridge(CloudWatch Events)で動かしているバッチをDatadogで監視する仕組みを構築した話

開発本部 認証連携チームでエンジニアをしている、id:ruru8net です。

これはClassi developers Advent Calendar 2021の9日目の記事です。
昨日の記事はこちらです。 Hardening 2021 Active Fault 参加レポート - 桐生あんずです

以前のClassi Advent Calender 2019では新卒が入社半年で社内サービスをリリースしてエンジニア楽しいってなったお話を書かせていただきましたが、あれから2年の間に業務の中で様々な経験をし、さらに知識やスキルを身につけていくことができました。

今日はその中でも自分が担当しているサービスの、バッチ監視の仕組みを考えたので紹介させてください。

背景

担当チームでは毎日深夜2時にDBからデータを削除するバッチを動かしています。
他にも社内では様々なバッチが動いていますが、これらを監視する仕組みは社内で確立されていませんでした。
そのためサービス稼働に影響の少ないバッチは実行中に問題があったり、そもそも実行されていなかったりしても検知されず、見過ごされてしまうことが多かったです。
弊社ではサービスの監視にDatadogを使用しているため、この監視体制にそのままバッチの監視を組み込むことでバッチの監視ができていない状態を是正したいと考えました。

前提

バッチファイル

Ruby on Railsを使い、rake taskとして実行させています。

バッチの仕組み

Amazon EventBridgeにてECS Fargateのタスクを起動させ、実行しています。これは既にdatadog-agentコンテナが動いている前提です。datadog-agentコンテナの設定方法は以下のURLを参考にしました。

https://docs.datadoghq.com/ja/integrations/ecs_fargate

f:id:ruru8net:20211208124211p:plain
バッチの構成図

使用する監視、通知ツール

  • Datadog
  • Slack

監視したいこと

バッチ実行において監視したいことは以下です。

  1. 定期的な実行の成功と失敗

    • バッチ実行中に例外が発生した場合の検知
    • バッチ実行用のタスクの起動自体がされなかった場合の検知
    • 例外を発生せずに何らかの理由でバッチ実行のコンテナが終了してしまった場合の検知
  2. 実行時間の異常

今回は定期的な実行の成功と失敗をメインとして、

  • バッチ実行中に例外が発生した場合の検知

    • →発生した例外をDatadog Eventとしてエラーを送信。DatadogのMonitorにてエラー通知を監視するMonitorを作成しエラーが送られてきた場合はslackにアラートを送信する。
  • バッチの起動自体がされなかった場合の検知

    • →バッチ実行の成功をDatadog Eventとして送信。DatadogのMonitorにて成功通知を監視するMonitorを作成し、成功通知が送られてこなかった場合はSlackにアラートを送信する。

という監視の仕組みを作っていきます。

f:id:ruru8net:20211208124333p:plain
バッチ実行を監視する仕組み

手順

1. dogstatsd-rubyを使ってバッチのスクリプトファイルにDatadogへEventを送信するよう書く

DatadogにEventを送信する方法は4つあります。

docs.datadoghq.com

  • Custom Agent Check
  • DogStatsD
  • Email
  • Datadog API

今回のようにsidecarコンテナとしてdatadog-agentを起動させているのであればDogStatsDを使ってEventを送るのがやりやすいと思います。

今回はRubyで書いているので基本的にはDatadogのドキュメントに書いてあるExampleと、使用するgemであるdogstatsd-rubyのドキュメントを参考にコードを書きました。 docs.datadoghq.com github.com

▽作成したバッチのスクリプトファイル

require 'datadog/statsd'

task batch: :environment do
  begin
    begin
      # バッチの実行時間を計測
      execution_time = Benchmark.measure do
        ###
        # 実行処理内容は省略
        ###
      end

      statsd = Datadog::Statsd.new(logger: logger, single_thread: true, buffer_max_pool_size: 1)
      begin
        # バッチの実行が完了したら成功Eventを送る
        statsd.event(
          'データを削除するバッチ', # Eventのタイトル
          "バッチ実行時間 #{execution_time.real}s", # 好きな内容をメッセージとして送れる
          alert_type: 'success',
          tags: ['env: development', 'service:rails-app'] # タグを指定
        )
      rescue => e
        logger.error e
      ensure
        statsd.close()
      end
    rescue => e
      begin
        # バッチ実行中に問題が発生した場合はエラーEventを送る
        statsd = Datadog::Statsd.new(logger: logger, single_thread: true, buffer_max_pool_size: 1)
        statsd.event(
          'データを削除するバッチ',
          "#{e.class}:#{e.message}",
          alert_type: 'error',
          tags: ['env: development', 'service:rails-app']
        )
        logger.info 'Datadogへのエラー通知送信完了'
      rescue => e
        logger.error e
      ensure
        statsd.close()
      end
    end
  end
end

def logger
  Rails.logger
end

解説

statsd = Datadog::Statsd.new(logger: logger, single_thread: true, buffer_max_pool_size: 1)

statsdのインスタンスを作成します。
dogstatd-rubyのバージョンや必要に応じてオプションをつけてください。

https://github.com/DataDog/dogstatsd-ruby#migrating-from-v4x-to-v5x https://www.rubydoc.info/github/DataDog/dogstatsd-ruby/Datadog/Statsd

begin
  # バッチの実行が完了したら成功Eventを送る
  statsd.event(
    'データを削除するバッチ', # Eventのタイトル
    "バッチ実行時間 #{execution_time.real}s", # 好きな内容をメッセージとして送れる
    alert_type: 'success',
    tags: ['env: development', 'service:rails-app'] # タグを指定
  )
rescue => e
  logger.error e
ensure
  statsd.close()
end

eventメソッドが取れるパラメータやオプションはこちらに書いてあります。 www.rubydoc.info

またドキュメントに書いてある通り、DogStatsDのクライアントが不要になった時には適切に破棄をするためにstatsd.close()します。

2.バッチを走らせてeventがDatadogに送られているかを確認する

https://app.datadoghq.com/event/stream にてeventの一覧が確認できます。 左上の検索欄に、eventのタイトルやタグで検索ができます。
このときの検索で、eventが一意に絞り込めるようなタイトル、タグをつけるようにしてください。

またメッセージの内容も一緒に出力されます。
ですので、ここに実行時間や、実行完了したときに欲しい情報を出力させておくと確認がしやすいです。

event例

状態
成功時
f:id:ruru8net:20211208124846p:plain
成功のevent
例外発生時
f:id:ruru8net:20211208124740p:plain
エラーのevent

3. Datadog Monitorを作成する

Monitors > + New Monitor > Event を選択します。
するとMonitor作成画面になります。今回は「バッチ実行中に例外が発生した場合の検知」と「バッチの起動自体がされなかった場合の検知」をする2つのMonitorを作成します。

バッチ実行中に例外が発生した場合の検知

エラーeventのみを絞り込むように設定し、alert conditionsをセットします。今回は24時間に一回動くバッチのため、24hoursを選択、また1つでもエラーeventを受け取ったらalertとして発火させたいのでAlert Thresholdを1にしています。

f:id:ruru8net:20211208125012p:plain
エラーeventを受け取った時にalertを発砲するDatadog Monitor 作成画面

バッチの起動自体がされなかった場合の検知

成功eventのみを絞り込むように設定します。
またeventをカウントする期間を24hoursにしてしまうと、前回のeventからきっかり24時間以内にeventが来ないとalertとなってしまうので、余裕を持たせるために25hoursにしておきます。
対象期間1つも成功eventがない場合はバッチの起動がされなかったとみなしalertを送るように、Alert Thresholdを1にします。

f:id:ruru8net:20211208125343p:plain
成功通知がない場合にalertを発砲するDatadog Monitor 作成画面

③で通知させたい先のslackチャンネル(slack-{チャンネル名}となっているもの)を選択します。
(DatadogとSlack連携のセットアップはこちら
https://docs.datadoghq.com/ja/integrations/slack/?tab=slackapplicationus)

④ではslackに投稿する際のテンプレートを作成します。
ここでは色々な変数やMarkdownが使えます。

4. Monitorで設定した通りにslackに通知が来ることを確認

Monitor作成時の右下にあるTest Notificationsで確認ができます。

f:id:ruru8net:20211208125518p:plain
Test Notifications

下のようにSlackに通知が送られるようになりました。

バッチ実行中にエラーが発生した場合の通知 バッチの実行確認ができなかった場合の通知
f:id:ruru8net:20211208125610p:plain
f:id:ruru8net:20211208125906p:plain

おわりに

実装について

DogstatsDによるEvent送信はバッチ処理中への埋め込みがしやすくとても使いやすかったです。
またバッチに限らず監視の仕組みを考える時にはまず、「何を監視したいのか」を整理するのがとても大事だなと思います。
今回は実行の監視のみしかできていませんが、今後は実行時間がかかり過ぎていた場合にalertを発報できるような仕組みも監視の項目に入れていきたいです。
(現状はeventのメッセージに対してMonitorを作成する方法が見つからず、別の方法を模索中です。)

監視の仕組み構築について

社内で確立されていなかったバッチの監視に対して、この仕組みを社内展開することができ、他のチームの人たちからも喜んでいただけたので嬉しかったです。
自分のチームだけでなく他のチームにとっても役に立つような仕組みづくりというのを意識して今後も頑張っていきたいです。

明日のClassi developers Advent Calendar 2021の担当はTomoya Namekawaさんです。お楽しみに。

リモートワークのための質問力向上研修を実施しました

この記事は Classi developers Advent Calendar 2021 の 7日目の記事です。

こんにちは。顧客サポート基盤チーム兼、技術戦略室にてエンジニアをしています、中島です。

みなさんは、日々仕事をする上で必須である「誰かに質問をする」という行為について、自信を持って適切に行うことはできているでしょうか?

先月弊社では外部講師である、株式会社フィッシャーデータのあんちべさん をお招きし、質問力向上のための研修を実施しました。今回はこの研修を実施するに至った背景、研修内容を少しお見せするのと、社内の反響をお伝えします。

質問力を向上しよう!と至った背景

弊社は2020年2月頃よりリモートワークへの移行を行い、1年半以上が経過しました。リモートワークのお困りごととして一般的にもよく聞かれる、コミュニケーションについての課題を見聞きするようになってきました。 (ちなみに私自身は2020年4月入社で、まだ片手で数えられるほどしか出社したことがありません)

特にコミュニケーションの一つである「質問をする」ということに対して、リモートワーク以前では気軽にできていた(ような気がする)のに機会が減ってしまったり、やり取りするのに時間がかかるようになってしまったり。

質問をする人も、回答する人も、双方ともになんだかしんどいぞ?と感じることが多くなってきました。 そんな時、とあるメンバーが「質問投げる時も受ける時も意識してもらえると助かるノウハウが多いよ」とあんちべさんのツイートをSlackで共有してくれました。

私は常々自分の質問の仕方が下手だなと感じることが多かったので、このツイートとツイート内にある記事にはとても感銘を受けました。本部長があんちべさんの質問の仕方研修を受けていたこと、その内容が今のClassiの課題にもマッチすることが多いのではないかという思いもあり、ぜひ具体的にお話しをお聞きしたく研修をお願いすることになりました。

質問とは何か

研修の始めにまずあんちべさんより問われました、「質問とは何ですか?」と。

このような抽象的な質問をする背景としては、今回の研修の目的「質問力を上げたい!」という共通のゴールのためには共通の言葉の定義をし、まず認識を合わせる必要があるということでした。まずは「質問とは?」の定義を以下のようにはっきりとさせます。

「質問とは問題解決のアプローチである」

定義のあとでよくない(けどよくある)質問の例を挙げていただきました。

  • なぜよくないのか?
  • なぜうまくいかないのか?
  • そもそもなぜ質問するのか?
  • 質問で実現したいことは一体何なのか?

これらのことを理解した上で、どのように質問に立ち向かっていけばよいのかを学びました。

知らないことを聞くということは質問をする上での一つのHowであって、本当にやりたいことではありません。質問とは何なのかを正しく把握することが、正しい質問への第一歩と教えてもらいました。

良い質問のために

どのような質問をすれば、成果に繋がるのでしょうか?ここでもまた「成果とは何か?」という問いを投げかけてくれました。成果を生み出すためには以下の3点セットが重要であるとのことです。

  • マインド(方向性)
  • 知識・スキル(方法論)
  • 行動(実践)

ここまでの前段で質問とは何かというマインドを学びました。どれか一つでは駄目で、3つ揃うことでより複雑な問題のゴール(成果)に向かっていけるということです。

どのような質問をすればよいのか?

ここからは後半です。具体的にどのような質問をすればよいのか?の方法論を学びました。

質問の手法

質問の手法として2つ挙げてくださいました。手法の詳細については調べていただければと思います。

  1. クローズド・オープンクエスチョン
    • 質問の背景次第で、使い分ける
  2. チャンクダウン・アップ
    • 質問が噛み合っていない時に、抽象度を合わせる目的で使う

質問のレベル

質問の手法は背景次第で変わってくるため、どのような状態を自覚しているのか、質問者・回答者双方で認識を合わせる必要があります。そのために質問には「レベル感」があるということを学びました。

まずは以下の3段階から、どの状態の時にどういった質問をするべきか具体事例を元に理解しました。

f:id:kazumeat:20211203211347p:plain
質問のレベル

※詳しく知りたい方は以下の記事を見ると更に理解が深まるかも知れません

質問者と回答者の心得

「なんで教えて(回答して)くれないんだろう?」「なんでそんな質問するんだろう?」双方このように思ってしまい、社内の関係がギクシャクすることはありがちです。

「質問をする」という行為は、得てして「質問者による質問の仕方の改善」がフォーカスされることが多い気がします。ですが、回答者の存在も忘れてはいけません。双方ともに以下のような心得を持つことが重要だと学びました。

f:id:kazumeat:20211203211428p:plain
質問者と回答者の心得

質問者の心得にもあるように、感謝の気持ちを示すという手段の一つとして、弊社で導入している Unipos といったピアボーナスでも伝えることができそうです。

良い質問のためのチェックリスト

質問のレベルも自覚できていて、心得も問題ないと思っている。しかし不安が拭いきれない場合のために、質問文を作るときのチェックリストを頂きました。

f:id:kazumeat:20211203211501p:plain
良い質問のためのチェックリスト

私自身できていなかったことばかりで、頭の中の整理をする上で非常に有用なリストと感じました。この内容をしっかりと考えることで解決策を思いついて質問しなくても良くなった!ということもありそうです。 このチェックリストは社内でもとても反響がありました (印刷して机に貼っておく!と言う人や、SlackでPinしている人も複数名いました)

質問がうまくいったかのチェックリスト

質問のやり取りが終わった後、質問に対して不足がないかを確認する必要があります。以下が明確に得られていれば、次の動き出しもスムーズです。

  • "As is → ギャップ → To be" が得られているか
    • 構造が明確になったか?
  • ネクストアクションが得られているか(自分で思いつけたか)
    • 次の具体的なアクションが明確になっているか?

質問をして回答をしてもらったけど、もやもやが残ることがたまにありました。これはAs is, To beを描けずに質問をしていたんだなと私自身気付くことができました。

質疑応答

弊社から挙がった質問を一部抜粋して掲載します。詳細を載せるのは控えますが、興味のある質問もあるのではないでしょうか?

質問者されたときに圧をかけずにいい質問者としての成長を促すにはどうしたらよいでしょうか? (やり方次第では詰める感じになってしまって難しそうに思いました)

なぜこの質問に回答するのか?を考えてから回答するとよいとお話しいただきました。

  • 回答は育成のためと考える
  • 質問者と回答者の間には知識等の高低差があるのが通常
    • 知っていて当然、のような態度を取らない
    • 質問者のレベルを把握(状況把握)し伴走する

いただいた回答内容は上の方で資料を掲載した「質問者と回答者の心得」にもあるところですし、あらためて意識していけるとよさそうです。

そもそも質問が出にくい組織の場合、どのようなコミュニケーションの課題があると思いますか?

あんちべさんからよくある課題を共有していただいた上で、改善のためによく行っている打ち手を3つお話し頂きました。

  • 交流会を開催する
    • レビュー会や相談会
  • 可視化される進捗管理をする
    • 上位者からの質問機会
  • ざっくばらんな雑談会を設ける

回答者と前提や制約や諸々を共有できるように、丁寧に質問文を作った結果、Slack 上で結構なボリュームの文になり、回答者が「ウッ」ってなり、確認を後回しにされる経験が多くあります。オンライン上の適切な質問において、上記のような問題を軽減するテクニック等ありますでしょうか?

回答者としては、回答するために背景説明をしてほしい気持ちと、長文読みたくない!の矛盾した気持ちを持ってしまうことがあります。そのために質問者として心がけたほうが良い質問の仕方を教えてもらいました。

  • 全体感が理解できるサマリを作る
  • 構造化して補足する

研修の反響

元々エンジニア向けに企画した研修でしたが、部署を超えエンジニア以外の方も含めて50名近く集まってくれました。講義内容は弊社メンバーも思い当たることや気付きも多かったようで、講義中のチャットも大変盛り上がりました。

研修後の実務では、これ質問力研修で習ったやつだ!と言ってくれるメンバーもいて、共通言語としてインストールできた気がして嬉しかったです。

Slackで感想を書いてくれたメンバーもいました🎉

f:id:kazumeat:20211203211610p:plain
Slackでの反響

質問力の向上に役立つ記事になりましたでしょうか?打席に立ち続け回数をこなしていかないと質問力は磨かれないということですので、学んだフレームワークを活かして実務に挑んでいこうと思います。

とてもためになるお話しをあんちべさんよりお聞きできて、大変感謝しています。ありがとうございました!

研修時に使用されたあんちべさんのスライドはこちらになります。

© 2020 Classi Corp.