EatSmartシステム部ブログ

ウェブサイトの開発や運営に関する情報です。

データベースサーバのディスク枯渇への対処

2018年3月にデータセンターからさくらインターネットへ移行して、まもなく2年が経ちます。 移行に際しての計画から実施は下記に書かれています。

eatsmart.hatenablog.com

クラウド・専用サーバを中心にVPSも併用していますが、データベースサーバに利用している専用サーバのディスク残量が時間とサービス成長に伴い減少してきました。 そして今週に入り、とうとうZabbixからアラートが飛んでくるようになりました。

Zabbixのアラートのしきい値は20%に指定しています。 専用サーバのディスク容量は400GBなので20%を切ってすぐに影響が出るわけではありませんが、以前から行いたかった利用していないインデックスの削除を行いたいと思います。

利用していないインデックスを探す

インデックスはテーブル設計時や機能追加時に作成すると思います。 作成時は必要だったものが後の改修等で利用しなくなることもあります。 PostgreSQLで利用しないインデックスを、稼働統計情報から以下のクエリで抽出しました。

SELECT 
    'DROP INDEX IF EXISTS ' || t1.relname || ';' as drop_index_query,
    'VACUUM ANALYZE ' || t2.relname || ';' as vacuum_query,
    t1.relname,
    t1.relfilenode,
    to_char((t1.relpages::int8 * 8192), 'fm999,999,999,999') as display_bytes,
    (t1.relpages::int8 * 8192) as bytes,
    t2.idx_scan,
    t2.idx_tup_read,
    t2.idx_tup_fetch
FROM pg_class t1, pg_stat_user_indexes t2
WHERE t1.relname = t2.indexrelname
and t1.relname like 'idx_%'
AND t2.idx_scan < 10
ORDER BY BYTES DESC

イートスマートでは、インデックス名を"idx_"で始めることにしているので、名前で絞り込みました。 idx_scanがインデックススキャンの実施回数となります。この数値が0のものは未使用となるため、削除する対象となります。

今回問題となったデータベースからは、合計約16GBのインデックスを削除することが出来ました。 ディスク容量の削減だけではなく、無駄なインデックスを作成する処理も無くすことが出来ました。

ランキング作成時に使ったSQL手法

先日、弊社サービスのもぐナビで、食品ランキングのリニューアルを行いました。

prtimes.jp

今回は、ランキング構築等で使用したいくつかのSQL手法を共有したいと思います。 ちなみに、弊社で使用しているRDBMSPostgreSQLです。

カテゴリ毎のランクの振り方

これはなんてことのない内容なのですが、今回はカテゴリごとにランキングを作っているので、ランクもカテゴリごとに振っています。 一括でまとめてランクを出すために、ランク関数(RANK)と集約関数(OVER)を使って出しました。

例えば、カテゴリ(大カテゴリ:LARGE_CATEGORY、中カテゴリ:MIDDLE_CATEGORY)ごとに、スコア(SCORE)の高い順に並べてランキングを作る場合は、

SELECT
    LARGE_CATEGORY,
    MIDDLE_CATEGORY,
    NAME,
    RANK() OVER (
        PARTITION BY LARGE_CATEGORY,MIDDLE_CATEGORY
        ORDER BY SCORE DESC
    ) AS RANKING
FROM SCORE_TABLE

というSQLを組んで、

INSERT INTO RANKING_TABLE
SELECT ・・・ FROM SCORE_TABLE

みたいな感じで、順位を振って一気にランキングテーブルにデータを作成しています。

上位3位までの対照表を作る

もぐナビのランキングでは、「食べたいランキング」と「オススメランキング」の2種類があり、それぞれ食べたい気持ちの期待度と食べた感想の評価をランキングしています。

今回は、カテゴリ毎に上位3位まで抽出し、左に食べたいランキング、右にオススメランキングを並べて対比してみようという事になりました。

大カテ 中カテ 順位 食べたい おすすめ
お菓子 チョコ 1位 ダース ポッキー
お菓子 チョコ 2位 パイの実 レーズンクランチ
お菓子 チョコ 3位 チェリーブランデー ガーナ
お菓子 ガム 1位 Fit's デカビタC キシリトール
お菓子 ガム 2位 Fit's Crispop キシリッシュハイパークール
お菓子 ガム 3位 マイニチケア キシリッシュライムクール

普通に考えると、カテゴリのマスタ表を主表にして、食べたいランキング表とオススメランキング表をJOINすれば良いという事になります。

ただし、食べたいランキングもおすすめランキングも、カテゴリが全て揃っているとは限らないので、LEFT OUTER JOINにする必要があります。また、順位(1位〜3位)も全て揃っているとは限らないので、順位の軸となる表も必要となります。

今回は、順位の軸となる表をGENERATE_SERIESを使用して

SELECT
    CAT.LARGE_CATEGORY_NAME,
    CAT.MIDDLE_CATEGORY_NAME,
    RANKING.NO,
    TABETAI.NAME,
    OSUSUME.NAME
FROM CATEGORY_TABLE CAT
INNER JOIN GENERATE_SERIES(1,3) AS RANKING(NO)
    -- 1から3までの順位をRANKING表のNO列として作る
ON 1=1    -- 結合しないのでダミー条件を入れる
LEFT OUTER JOIN (
    SELECT * 
    FROM TABETAI_RANKING_TABLE
    WHERE RANKING <= 3
) TABETAI
ON CAT.LARGE_CATEGORY = TABETAI.LARGE_CATEGORY
AND CAT.MIDDLE_CATEGORY = TABETAI.MIDDLE_CATEGORY
LEFT OUTER JOIN (
    SELECT * 
    FROM OSUSUME_RANKING_TABLE
    WHERE RANKING <= 3
) OSUSUME
ON CAT.LARGE_CATEGORY = OSUSUME.LARGE_CATEGORY
AND CAT.MIDDLE_CATEGORY = OSUSUME.MIDDLE_CATEGORY
;

という感じで実現しました。

その他、小ネタ

ランキングのスコア算出時などに、偏差値やパーセンタイル値を使用しています。 このあたりは、最新のPostgreSQLであれば標準の関数で算出できるのですが、弊社で使用しているバージョンでは関数が使えなかったので、SQLを使って算出しました。

偏差値の算出

偏差値は平均値と標準偏差を使用して算出します。 具体的には、得点(POINT)と平均値の差を標準偏差で割ったものを10倍し、中心を50にするために50を足すことで算出できます。

先ほどのテーブルでSQL実装をすると

SELECT
    LARGE_CATEGORY,
    MIDDLE_CATEGORY,
    NAME,
    (((POINT-POINT_AVG)/POINT_STDDEV * 10) + 50) AS DEV_VALUE
FROM (
    SELECT
        LARGE_CATEGORY,
        MIDDLE_CATEGORY,
        NAME,
        POINT,
        STDDEV(POINT) OVER (
            PARTITION BY LARGE_CATEGORY,MIDDLE_CATEGORY
        ) AS POINT_STDDEV,
        AVG(POINT) OVER (
            PARTITION BY LARGE_CATEGORY,MIDDLE_CATEGORY
        ) AS POINT_AVG
    FROM SCORE_TABLE
) S

という感じです。

パーセンタイル値の算出

パーセンタイル値とは、データを小さい順に並べた時に、全体のN%目に該当する値のことです。 全体の90%が入る境目の値を90パーセンタイル値と呼びます。

ランキング算出時の特異値などを平準化するための足切りなどに使います。

算出するためには、値の小さい順に順位をつけて全体の件数で%を算出すれば良いので、

SELECT
    LARGE_CATEGORY,
    MIDDLE_CATEGORY,
    POINT,
    PERCENTILE
FROM (
    SELECT
        LARGE_CATEGORY,
        MIDDLE_CATEGORY,
        POINT,
        RANK() OVER (
            PARTITION BY LARGE_CATEGORY,MIDDLE_CATEGORY
            ORDER BY POINT
        )*100 / COUNT(*) OVER (
            PARTITION BY LARGE_CATEGORY,MIDDLE_CATEGORY
        ) AS PERCENTILE
    FROM SCORE_TABLE
) S

のようになります。

SQLって工夫すると色々できるので、本当に楽しいですね!

今週は以上です。

CSVのエクスポート処理とSQL

今回は、クスパのオンライン決済のプロジェクトの実装で学んだことについてまとめていきます。 テーマは、CSVのエクスポートと出力内容を取得するSQLの内容を振り返りです。

実現したい処理の流れ

簡単に概要ですが、下記のような仕様を実現します。
オンライン決済を利用した先生の振込情報を抽出→期間、振込日、支払状態によってソート→CSVの出力

CSVの出力情報を追加する関するアクション

public class MonthlyPaymentCsvOutputAction {
    protected void output(T form) throws ServerException {
        String fromDate = ((MonthlyPaymentCsvBean) form).getFromDate();
        String toDate = ((MonthlyPaymentCsvBean) form).getToDate();

        //formで入力された情報(日付、振込日、支払状態)を元に、SQLを発行し対象となる振込情報のデータを取得します。
        List<MonthlyPaymentCsvBean> list = CsvUtil.※getMonthlyPaymentCsv(((MonthlyPaymentCsvBean) form).getTransferDate(), ((MonthlyPaymentCsvBean) form).getRevenueStatusKbn(),fromDate,toDate);
        //StringBuilderをインスタンス化し、出力する内容を文字列で追加していく。
        StringBuilder sb = new StringBuilder();
        //見出しとなる情報を追加。
        sb.append("抽出期間 "+ fromDate.substring(0,4) + "/" + fromDate.substring(4,6) +"~" + toDate.substring(0,4) + "/" + toDate.substring(4,6)  ).append("\n");
        sb.append("運営者ID,先生名,先生売上月,銀行名,先生振込月日,売上金額,クスパ決済利用料,振込対象金額,状態" ).append("\n");
        //listで取得した内容をforループで回し、1件毎のデータsbに追加。
        for (MonthlyPaymentCsvBean bean : list) {
            sb.append(bean.getOrganizerId()).append(",");
            sb.append(bean.getTeacherName()).append(",");
            sb.append(bean.getGinkoName()).append(",");
            sb.append(bean.getTransferDate()).append(",");
            省略
        }
        try {
            //文字コードを引数に指定して、バイト形式に変換する。
            buffer = sb.toString().getBytes("windows-31j");
            //文字コードのエンコードがサポートされていない場合に例外をスロー
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }
}

この後、CSVを出力するクラスで呼び出し、ファイルの書き込みが行われ、CSVファイルが 作成される流れになります。 上記でapendしていく処理については、下記の書き方も可能です。

sb.insert(sb.length(),bean.getRevenueAmount()).append(",");

フィードバックを受けて学んだこと

出力条件の指定で、先生の状態によって出力内容を変えるということができていなかったので、 まとめたいと思います。以下のように日付の他にradioボタンの形式で状態(振込の状態を指す)によって、 出力内容を変えることが可能です。

<tr>
    <th>状態</th>
    <td>
        <input type="radio" name="revenueStatusKbn" value="03" checked="checked">未支払い
        <input type="radio" name="revenueStatusKbn" value="01">支払済
        <input type="radio" name="revenueStatusKbn" value="02">未確定
                //全てのvalue属性を空で扱う
        <input type="radio" name="revenueStatusKbn" value="">全て
    </td>
</tr>

ここで、「全て」を選択した場合にSQLで対象のデータを取得するためには、以下のように書くことで 「全て」の条件に合致するデータを拾うことができます。

出力対象のデータを取得するSQLを一部抜粋(※getMonthlyPaymentCsv()の中で利用)

 SELECT
     CSV出力に必要なカラムをviewから取り出す
    FROM
    view_table v
    WHERE
    (v.revenue_status_kbn = /*revenueStatusKbn*/
        OR CAST(/*revenueStatusKbn*/ AS CHARACTER) IS NULL
        OR CAST(/*revenueStatusKbn*/ AS CHARACTER) = ''
    )
)

フォームで渡されてきた値をNULL又は空文字としてキャストすることで、対応できます。 もしくは、「全て」のinputのvalue属性を"00"としてあげて、WHERE句を以下に変更することで目的のデータが取り出せます。

WHERE
(v.revenue_status_kbn = /*revenueStatusKbn*/
        OR /*revenueStatusKbn*/ = '00'
    )

まとめ

以上となりますが、CSVの出力の一連の流れととその過程で学んだSQLの記法について 振り返ってみました。ファイル出力の処理だけでなく、バリデーション、エラー処理、 インポートの処理についても触れることができので、実装の材料として良かったと感じました。

クスパ決済サービス開始しました

以前、クスパ決済の導入に向けた記事を紹介しましたが、2020年1月7日に無事サービス開始しました。 当初、2019年12月にサービス開始する予定でしたが、ある要因で2020年1月7日サービス開始となりました。 まずは、リリース延期となった経緯を紹介します。

eatsmart.hatenablog.com

リリース延期になった経緯

決済代行経由でクレジット会社の利用審査をお願いしておりましたが、当初予定していたよりも審査が長引いたことが原因でした。
決済代行会社のお話によると、BtoC系サービスの場合、ECでのクレジットカード利用は割と審査が通りやすいらしいのですが、クスパ決済(レッスン予約時のクレジットカード利用)は、役務提供サービスに該当する為、予定していたよりも各クレジット会社の審査に時間が掛かったとのお話でした。
最終的には、VISA/Master/JCB/Diners Club/American Express 5社全て審査が通り、無事サービス開始することが出来ました。
決済代行会社にもよるかと思いますが、今後、役務提供のオンライン決済を導入される方は、余裕をもって準備されることをオススメします。


次に、今回クスパ決済のサービス開始するに辺り、リリースタイミングを調整して利用促進を図った点を紹介します。

利用促進を図った点

そもそも、クスパ決済はレッスン予約を行う際にクスパ経由でオンライン決済してもらうサービスになりますが、クスパ決済で支払いして頂く為には下記3つの条件が必要となります。

  1. 料理教室の先生に、クスパ決済の利用申請をしてもらう必要がある

  2. 利用申請して頂いた料理教室の先生が、レッスン登録する際にクスパ決済の支払い方法選択してもらう必要がある

  3. ユーザーがレッスン予約する際に、支払い方法でクスパ決済を選択してもらう必要がある

上記条件で(1)(2)の数が増えないと、最終的にユーザーがクスパ決済での予約が行えません。
そこで(1)→(2)→(3)の順で、段階的にリリースする事にしました。

リリースタイミングを分ける事で、開発プロジェクトのバージョン管理や、リリース毎に必要なDDL/DMLの整理等が大変でしたが、結果、1月7日のユーザー向けサービス開始のタイミングで、ある程度クスパ決済での支払いが行えるレッスン数を揃える事ができました。

サービス設計される際に、リリースタイミング等は見落としがちなので参考にしてみて下さい。

サービス開始後のユーザーの反応について

1月7日サービス開始してからのユーザーの反応ですが、徐々にクスパ決済の利用者が増えつつある傾向ですが、事前アンケート等で想定していたより利用頻度が少ない状況です。
現時点での要因ですが、

  • 全体のレッスン数に対して、クスパ決済での支払いが行えるレッスン数少ない

  • ユーザーにクスパ決済のアピールが行えていない

等があります。上記に対して、プロモーションやサイト改善等を行っていく予定です。 今後、料理教室の予約をされる方がいらっしゃる様でしたら、クスパ決済を是非活用してみて下さい。

cookingschool.jp

2019年を振り返って

もうすぐ2019年も終わりになりますが、システム部の2019年を振り返ってみたいと思います。

サービス面での振り返り

弊社のサービス的には、時系列で振り返ると

  • 1月〜3月 クスパ 先生向け有料サービスの改善
  • 4月〜7月 もぐナビスマフォ版UI改修
  • 8月    クスパ 稼働サーバー移設
  • 9月〜12月 クスパ レッスン決済サービス開発(年明けリリース!)

のようなことを実施してきました。

ブログ記事を振り返ってみても、ところどころ、これらに絡んだ記事が投稿されていたなあと思いました。

内容的な振り返り

投稿を振り返って印象的だったものを挙げます。

React

もぐナビのUI改修でReactを使用したのですが、随所で新しいものを使うのは各自の判断でちょこちょことあったのですが、まとまった形での実装技術の採用は久しぶりだったと思います。

今までのなんとなくウォッチはしていたのですが、Redux含め内容や構造をきちんと理解して使えるようになるまでは、少し慣れが必要でした。ただ、慣れてくると、確かにちょっと凝ったUIの実装が簡単にできて、便利でした。まあ、イベントドリブンで裏にロジックを書くところが、昔々やったVisualBasicを思い出す感じでしたけど!

インフラ周り

今年もサーバーの移設やら再構成やらで、dockerコンテナ関連のインフラを色々と扱いました。

オーバーレイネットワーク

弊社のサービスでは、ひとつのdocker基盤の中に複数のサービスを動作させているのですが、環境を分離するために、サービスごとにオーバーレイネットワークを作成してネットワークを仮想的に切り離しています。

もともと基盤を構築する時に、コンテナのオーケストレーションツールとしてswarmを使う計画で、オーバーレイネットワークもswarmで構築しました。ただ、デプロイ時のローリングアップデートでの諸問題やswarm自体の安定性に不安があったため、オーバーレイネットワークの基盤としてのみswarmを使っていました。

今年、クスパ 稼働サーバー移設でインフラの見直しをした際に、オーバーレイネットワークもswarmではなくKVSを使ったものに置き換えようと検討して、etcdを使ったオーバーレイネットワークをテストしました。ただ、残念ながらetcdでのオーバーレイネットワークも安定性に不安がある結果となったため、現在もswarmでのオーバーレイネットワークを使用しています。

コンテナ技術について

弊社のサービスは、基本的にJavaを使っています。インフラをdockerに移行した時に、tomcatベースのコンテナやspring-bootを使ってwebサービスを作っていました。その後にJavaを稼働させるコンテナで新しい種類のものが出てきたので、micronautやJibなどを色々と使ってみました。

サーバー移設

ここ最近は、年に1回くらいiDCをまたいでのサーバー移設の作業を行なっているので、サービスは違えど手順はだいぶ確立されてきました。今年はクスパ のサービスを手慣れた手順で移設しました。

Google CoreUpdate

毎年、年に数回はGoogleの変動は発生してきたのですが、昨年までは明らかに悪いSEOをしているコンテンツが落とされている印象でした。今年はGoogle自身がCoreUpdateと銘打って数回の変動を実施しましたが、良い影響も悪い影響もありました。

Googleとしては、ただキーワードの結果を表示するのではなく、ユーザーの検索意図に踏み込んで結果を表示するとのことで、その結果としてうちのサイトが合っていたり合っていなかったりしているんだろうなとは思いますが、以前内容が薄くて落とされた(と思われる)サイトがうちのサイトより上に表示されるようになっていると、少し複雑な気持ちにはなります。(けしてGoogle様に逆らう意図ではありません)

その他

ブログの継続

週1で記事を投稿しようと決めて運用していて、たまに投稿できない週もありましたが、概ね毎週続けられたことがとても嬉しかったです。

ひとりの努力の結果ではないので、個人的な達成感という訳では無いですが、決めたことを続けていって積み重ねるのはチームでも個人でも意識の賜物だと思います。今後は、より情報の質も意識して配信していきたいです。

新人の成長

で、ブログを振り返ってみた時に、新人が書いていた記事が、初めの頃はJavaSQLの基礎中の基礎的な内容だったのが、色々と業務をこなせる所まで少しづつですが成長していることを感じました。とは言え、エンジニアとしてはまだまだ知らないといけないことだらけなんですが、こうやって振り返って歩みが見えるのも良いことだと思いました。

以上、まとまりのない投稿となりますが、これを今年のまとめとしたいと思います。

ZabbixからSSL証明書の有効期限を監視する

先日、一部のサービスで利用しているSSL証明書の有効期限が切れる障害を起こしてしまいました。 SSL証明書にはLet’s Encrypを利用しており、事前に通知が来ていたにも関わらず、更新を忘れてしまっていました。 これをふまえて、業務フローを見直すとともに、SSL証明書の有効期限をZabbixで監視することにしました。

Zabbixから監視するには、以下の記事を参考にしました。

ZabbixでSSL証明書有効期限を監視する - Qiita

違いとしては、スクリプトをZabbixサーバに配置せず、監視対象のWebサーバのメトリクスとして残り日数を取得するようにしました。

SSL証明書の残り日数は、以下のスクリプトで取得します。 引数でホスト名を指定出来るようにしてあります。

date +"%s" --date="`openssl s_client -connect $1:443 -servername $1 </dev/null 2>/dev/null | openssl x509 -enddate -noout | cut -d'=' -f2`" | awk '{printf("%d\n",($0-systime())/86400-1/86400+1)}'

監視対象の zabbix_agentd.conf へ、以下を追加します。 Zabbixで指定したホスト名を、上記スクリプトへ渡すようにしています。

UserParameter=ssl_cert_days.[*], /var/git/infra/bin/ssl_cert_days.sh $1

続いてアイテムを作成します。 Typeでホスト名を指定しています。

項目
Name ssl_cert_days.example.com
Type Zabbix agent
Key ssl_cert_days.[example.com]
Update interval 1d

アイテムの次はトリガを作成します。 余裕を持って14日前に通知が来るようにして、SSL証明書を更新するようにします。

項目
Name ssl certificate of example.com has expired
Severity Warning
Expression {zabbix-server:ssl_cert_days.[example.com].last()}<14

以上で、ZabbixからSSL証明書の有効期限を監視する仕組みをつくることが出来ました。

Talend API Testerを使ってみる

今回は、API開発やテストなどに使える便利なツールTalend API Testerをご紹介します。
※最近、プロダクト名の変更があったようなので調べる際には、Talend API Testerに変えて Restlet Clientでも情報が多くヒットします。

Talend API Testerについて

詳細については、以下に譲りますがブラウザベースで、APIの結果を確認できるChrome拡張機能です。 基本的には、リクエストしたURLに対するAPIの返却結果の確認や渡すパラメーターの正当性を確認する テストなどに使えるかと思います。 Talend API Tester概要

ツールの導入に関して

https://chrome.google.com/webstore/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm/reviews?hl=ja&utm_source
上記サイトから、Googleアカウントにログイン状態で「Chromeに追加」を選択し、「拡張機能を追加」で TalendAPI Tester(無償版)が利用できるようになります。 f:id:eatsmart:20191206150214p:plain
Chromeのメニューバーからアプリの利用ができます。

POSTメソッドを試す

弊社で提供しているサービスカロリーチェックAPIを用いて、 まずはPOSTでのリクエストで、食品に関するカロリーデータを取得してみます。 APIの仕様上、まずは認証キーを取得する必要があるため、POSTメソッドで認証キーを取りに行きます。 画面に「METHOD」と書かれたタブをPOSTにし、URL入力覧にリクエストするためのURLを入れます。 パラメーターとして、siteId(コンテンツ利用に必要なID)とpasswordが必要になるため、「QUERY PARAMETERS」の欄で 参考画像のようにパラメータをセットします。 f:id:eatsmart:20191206150908p:plain

セット完了後、「send」を押すとレスポンスが確認ができます。 f:id:eatsmart:20191206151044p:plain

authkeyが取得できているのが確認できます。 また、ステータス0は「正常」を指すので正しくリクエストしていることになります。

GETメソッドを試す

前項で得られたauthKeyを利用し、六花亭のマルセイバターサンドの食品データを取得していきます。 次は、GETメソッドでリクエストするので、「METHOD」をGETにします。例にならって、「QUERY PARAMETERS」でパラメーターをセットします。authKeyに加えて、今度は食品に割り当てられたfoodCodeも追加し、リクエストを送信します。
f:id:eatsmart:20191206151322p:plain

200OKとともに、食品名、カテゴリー名、店舗名などが返却されているのが確認できます。
f:id:eatsmart:20191206153834p:plain

また、フリーワードでAPIに対してリクエストした場合、freeword(ピスタチオで検索)に加えて、offset,limitをパラメーターで 渡すことで以下のような結果が確認できます。 f:id:eatsmart:20191206155935p:plain

 その他便利な機能

よく使うリクエストを保存して、次回使う際に呼び出すことができるSave機能があります。 右上の「Save as」を選択し、プロジェクト(例:カロリーチェック)を作成し、そのプロジェクト内に 使用したリクエストを保存しておくだけです。左のメニューにプロジェクト及びリクエスト名が確認 できるので、そちらからデフォルトでURL、パラメーターが予め設定されたものが利用できるようになります。 f:id:eatsmart:20191206152006p:plain 返却結果をCSVファイルとしてエクスポートすることもできます。

まとめ

以上となりますが、ブラウザからAPIをたたき、テスト結果を簡単に確認できるので、IDEなどを立ち上げる必要がない分、 便利に感じると思います。特定の開発環境を構築する必要もないので、例えばAPIの返却結果を他人と共有して見たいなど 開発に限定しないシーンでも広く使えるのではないでしょうか。