au Commerce&Life Tech Blog

au コマース&ライフ株式会社の開発ブログ

第2回エンジニアインタビュー!

プランニング・ポーカーって何ぞや!?工数算出にこんな手法が!

こんにちは! 前回に引き続き、プロジェクトマネジメント部の美保です。
本ブログの運営メンバとして、更新管理など担当しています。

エンジニアメンバーへの突撃インタビュー企画第2弾は ダイレクト技術戦略部の座安朝秀さん(以下、Zさん)。
プランニングポーカーという開発手法について、お話を伺ってきました!

自分がやらなきゃ誰がやる!?


――Zさん、この度はインタビュー企画にご協力いただき、ありがとうございます。
まず、これまでの経歴や現在の業務内容について簡単にお話しいただけますでしょうか。

Z:2015年4月より旧LUXA社にJoinし、au WALLET Marketの立ち上げに携わってきました。
現在はLUXAサービスに関わるシステム全体の開発、アーキテクチャの策定等を担当しています。 チーム内の要件を取りまとめて、メンバーに指示を出す役割も担っています。

f:id:kcf_am:20191008110545j:plain
Z氏

――10月からはグループリーダーとしてもご活躍されていらっしゃいますよね。 役割が増えた今、仕事上でモチベーションが上がるのはどんなときでしょうか?

Z:そうですね~…。。 自分がやらなきゃ誰がやる!?的な状況下では、気持ちが盛り上がります。
自身のミッションとして、サービスがスピーディーに開発できる体制、仕組みを作ることを目指しているので、 メンバーが良いプロダクトを素早く作れるような環境を作ることに全力を注ぎたいと思っています。

――素晴らしい!普段はクールな印象のZさんですが、熱い思いが伝わってきました! そんな中、プランニング・ポーカー*1という手法を導入しようと思われたきっかけは何だったのでしょう?

プランニング・ポーカー合宿のはじまり!

f:id:kcf_am:20191007194723j:plain
こちらが、「プランニングポーカーカード」なるもの

Z:とあるプロダクトで、工数算出をしなければいけない状況があって。
これまで、自身が算出する工数と他人が算出する工数のギャップが激しいという 課題を抱えていたので、実際に手を動かして作業するメンバーとの擦り合わせが必要だと感じていました。
バッファを積みすぎる状況を回避したい、かと言って無理な工数も出したくないと思っていましたね。

――なるほど。そんな課題を一気に解決してくれるのがプランニング・ポーカーだったと。 具体的にはどんなやり方で実施されたのですか?

Z:別途社外会議室を貸し切り、他業務が入らないよう、丸1日合宿のような形式を取って集中的に行いました。 参加メンバーは合計で7名程度(自分の他に検証を実施するテストチーム+実際の作業を行うメンバー)です。

~やり方~
①まず、相対的な見積となるよう、ベースのタスクに対する工数(ポイント)を決めます。
②実施するタスクの工数(ポイント)がどれくらいか?各自でカードを提示します。
③提示ポイントに差があった場合、都度話合いをし、精査していきます。
上記①~③をタスク毎に繰り返し実施します。


1タスク5分程度でサクサク進むかと思っていましたが、
初回ということもあり、1タスクにつき15~20分程度かかりました。
メンバーの知識レベルにバラつきがあったこともあり、
実際は1日使って実施するつもりだった90タスクのうち、半分も消化できませんでした。。

f:id:kcf_am:20191008110600j:plain
ポストイットにタスクを書きまくれ!

――なんと…!活発な議論がなされた証拠ですね。実際やってみて、どんなメリットや課題が見えましたか?

Z:良かったのは、チームビルディングの一環として、メンバー間でナレッジの共有ができたことです。
今回テストチームも入ったので、テスト的な観点も工数見積もりに入れることができました。 また、自身がファシリテートしたことで、特定のメンバーが発言し続けるなどの偏りはなかったです。
改善点については、メンバーに対しての事前知識のインプットができていなかった点でしょうか。 思いの外、事前共有に時間がかかったため、プロジェクトの全体概要を予め伝えておけばよかったです。 (概要、要件説明に午前中丸々使ってしまい)実質のプランニング・ポーカーは午後から2~3時間の実施となりました。

――対象プロジェクトに対して、全員が近いインプットを持って臨めるとベターということですね。その他、振り返ってみていかがでしょうか?

根拠のないバッファがなくなり、個々の生産性が読めるように!


Z:プランニングポーカーで出す工数は標準的な工数になってきます。
ベテランの工数、新卒の工数属人化した工数でなく、作業に対しての純粋な工数になってくるはずと考えています。
(ベテランになればなるほど、バッファ積まれがち。笑)

f:id:kcf_am:20191008112242j:plain
ハイ、カード出し。ゲームのような見積もりで楽しそう!

また、裏の目的として「個人個人の生産性が図れるようになる」というのがあります。
(標準的な工数に対して早く終われる人は生産性が高い等)
これらを数値化して、業務委託の方の評価に出せればよいなぁと考えています。
既にフロント側でも実施していますが、今後も推進していきたいです。

――個人の生産性を図れるようになる、という副次的な効果まで…!是非全社的に取り入れたい手法だと感じました。最後に、今後さらにやっていきたいことがあれば教えてください。

Z:ナレッジを共有する仕組みをもっと取り入れていきたいと思いますね。 旧LUXAのナレッジはドキュメントが混ざっている状態なので、 旧KCF分も合わせて有効活用できるようにしていきたいです。 aCLとして合併したので、お互いの良いところを共有、活かせるような仕組みを今後も作っていきたいと考えています。

――良い部分を組み合わせて、さらなる効率化を目指していきたいところですね!本日は、お時間いただきありがとうございました!

Z:ありがとうございました。


型にとらわれず、常に革新的なアプローチで開発メンバーをリードし続けるZさん。
プランニング・ポーカーは、単なる工数見積もりにとどまらず、 チーム間での意識の擦り合わせやプロダクトの品質を保つ意識を各自が認識できる 大変有意義な手法だと感じました。

次回のインタビューもお楽しみに!

*1:プランニングポーカーとは、「1、2、3、5・・・」といった数字が書かれたカードを使って、タスクの規模を相対的に見積もる手法。まず、作業を行うメンバーを集めそれぞれにカードを配り、その後それぞれのタスクに対して開発を担当するメンバーが思いのままに数字のカードを出し合う。

第1回エンジニアインタビュー!

クーポンTエンジニア佐藤さんにアレコレ聞いちゃいました

はじめまして!
auコマース&ライフ*1Wowma!プロジェクトマネジメント部の美保です。
本ブログの運営メンバとして、更新管理など担当しています。


弊社エンジニアメンバーへの突撃インタビュー企画ということで、
記念すべき第1回として
システム本部Wowma!システム開発部プラットフォーム開発グループの佐藤直人さんにお話を伺ってきました!

コーディングに夢中になりすぎて、いつの間にか朝になってることも。(笑)

f:id:kcf_am:20190712141718j:plain

――この度はインタビューにご協力いただき、ありがとうございます。
まず、出身地、休日の過ごし方などを教えてください。

佐藤:新潟県佐渡ヶ島(偶然にも筆者と同郷なのです!)に、高校卒業まで在住していました。
中学生の頃から漁船に乗って働いていて、当時は毎朝4時起きでしたね。。
今となっては完全逆転生活です。笑
週末は深夜1時頃から個人学習でコーディングをしているのですが、つい夢中になってしまって。。
気が付くとMacの壁紙*2で朝になったことに気が付く場合もあります。笑

――なんと・・・!朝まで個人学習、立派すぎます!!感涙

佐藤:いえいえ。趣味は中高やっていた卓球で、今でも市民体育館にて練習を重ねる日々です。
社外に仲間がいて、試合も出ています。(シングル、団体戦

サービス規模の大きさが、学びやモチベーション維持に繋がっている

――auコマース&ライフ(以下、aCL)に入社する前にやっていたこと、入社のきっかけについて教えてください。
佐藤:前職は新卒で入社した企業で、主に求人サイトの開発、運営を担当していました。
時にはメルマガを書いたり、Facebookの更新をしたり…、開発業務以外も幅広く対応していました。
入社のきっかけは、他の会社のサービス、業種を見てみたいという思いや、
もっとエンジニアとして成長したい気持ちが強かったためです。

――今、aCLではどんな仕事を担当されているのですか?
佐藤:au Wowma!(エーユーワウマ)*3のクーポンシステムを担当しています。
現在エンジニア:2名、プロジェクトマネージャー:1名、プロダクトマネージャー:1名と、 個性豊かなメンバーで構成されています。
いい意味でいろんなことを言い合える、仲が良いチームです。
毎週水曜日にスプリントレビュー*4を実施していて、技術共有や意見交換を行っています。

――確かに、クーポンチームはいつも笑いが絶えない、賑やかなチームのイメージです!
佐藤:はい、そうですね!
初めてECを経験しましたが、トラフィック*5も多いサービスなので、
普通なら影響がないコードでも、注意が必要な場合が多いです。
この点が非常に勉強になっています。
また、気が付けば社会人4年間のうち、2年半くらいリプレイスをやっています。笑
規模は異なりますが、大変さは変わらないですね。。

――大変な作業が続く中、ご自身のモチベーションをどのように維持されていましたか?
佐藤:そうですね~…。確かに大変なことは多いのですが、
やっぱりシステムを作るのが好きなので。
最近はあまり意識していなかったですが、世に出ていくものを作れるということは楽しいです。
(今担当している)クーポンはユーザーに直接利用してもらうことができ
大きなサービスなのでやりがいを感じられる場面が多いですね。

――サービス規模の大きさがやりがいに繋がっているということですね。
ご自身の中でのミッションは何だと思いますか?
佐藤:店舗さんも含めて、より使いやすいau Wowma!のプラットフォームを作ること!
それから、安定してクーポンのシステムを運用していくことです。

f:id:kcf-developers:20190718112537j:plain

エンジニア間の社内交流を深めて、技術を向上させたい

――佐藤さんの考えるaCLの魅力は何ですか?
佐藤:労働環境が良い点です。
働き方の自由度が高く、自分の時間が作りやすいですね。

――それはエンジニア職に限らずですね。私自身も感じている部分です。
逆に、今後変えていった方が良いと思う点も教えてください。
佐藤:社内交流、特にエンジニア同士の交流の場がもっとあってもいいかもしれないなと思います。
たまに社内勉強会やボードゲーム部の活動もあるのですが、頻度を上げたいですね。

――確かに、合併後エンジニア同士の交流の場がなかなか作れていないのは事実なので、
これからどんどん増やしていけたらと思います。

最後に、今後aCLで実現したいこと、チャレンジしたいことを教えてください。
佐藤:先ほどの話にも通じますが、自ら積極的に社内交流を深めていきたいと思っています。
そして、実務で使える技術をどんどん磨いていきたいです。

――学ぶ姿勢を常に持ち、前進し続ける姿が素晴らしいです。
本日はお時間いただき、ありがとうございました。

佐藤:ありがとうございました!

f:id:kcf_am:20190712141810j:plain

ご多忙な中、約1時間のインタビューに答えてくれた佐藤さん。
エンジニアとしてのスキルアップ、サービスへの探求心を
常に持ち続けている姿勢に、筆者自身も刺激をもらうことができました。
改めて佐藤さん、ありがとうございました。

次回のインタビューもお楽しみに!

*1:2019年4月1日より弊社KDDIコマースフォワード㈱は、
合併にあたりauコマース&ライフ㈱に改称致しました。

*2:macOS 10.14 Mojaveでは時刻に合わせてデスクトップ壁紙を変化させる 「ダイナミックデスクトップ」機能が利用可能になっている。

*3:2019年7月25日以降、「Wowma!」は「au Wowma!」へ名称変更します。

*4:計画~開発~ふりかえりでの成果物(出荷可能な製品単位)を確認し、フィードバックをもらう場

*5:ネットワーク上で送受信される信号やデータ量

Javaの静的解析ツール「PMD」を導入してみた件

KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。  本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。

導入した背景について

こんにちは、KCFのエンジニアの坂本です。

今回は、静的解析ツールの導入の話です。
静的解析ツールといえば「どうでもいい細かいコードスタイルとか怒っていてウザい」と感じていませんか?

少なくとも、私はそうでした。

しかし、私たちのプロジェクト・チームの色いろな事情もあり、
「人間の眼によるレビューだけでなく、機械による自動的なチェックにも助けてもらう、必要がある。」
ということを痛感しました。
そこで、静的解析ツールを導入してみようということになったわけです。
今回はそんな話です。

静的解析ツールも色いろと種類があるわけで、
候補になりそうなものをざっとひと通り調べて見たのですが、
Javaの静的解析ツール「PMD」を採用することにしました。

pmd.github.io

導入の理由ですが、カスタムの設定が比較的シンプル・簡単に行えて、
自分たちのプロジェクトに合った設定を実現できそうだったからです。

例えば、極端な話、以下のようなカスタム設定XMLを用意すると、
「自分たちのコード・ベースに不要なimport文がないかどうか」
という事だけをチェックすることが出来ます。

<?xml version="1.0"?>
<ruleset name="customruleset">
    <rule ref="category/java/bestpractices.xml/UnusedImports" />
</ruleset>

そういうわけで、
「これなら自分たちのプロジェクトにも導入してつかえるんじゃないか?!」
と思ったわけです。

導入方法の簡単な紹介

さて、実際にPMDを動作させる方法ですが、非常にシンプルです。

先ほどのPMD本家サイトの「QuickStart」の部分にあるように、
例えば「MacOS」であれば、
以下のようにインストールしてみてください。

$ cd $HOME
$ curl -OL https://github.com/pmd/pmd/releases/download/pmd_releases%2F6.12.0/pmd-bin-6.12.0.zip
$ unzip pmd-bin-6.12.0.zip


そして、まず試しに実行してみる場合は、こんな感じですね。

$ alias pmd="$HOME/pmd-bin-6.12.0/bin/run.sh pmd"
$ pmd -d <プロジェクトのソースフォルダ> -R rulesets/java/quickstart.xml -f text


ここで例えば、さっきの
「不要なimport文がないかどうかだけをチェックする」
というカスタム設定XML
以下のようなパス&ファイル名で保存したとします。

■<プロジェクトのソースフォルダ>
/static-code-analysis/pmd/custom_analysis.xml

その場合の実行はこんな感じになります。

$ alias pmd="$HOME/pmd-bin-6.12.0/bin/run.sh pmd"
$ pmd -d <プロジェクトのソースフォルダ> -R <プロジェクトのソースフォルダ>/static-code-analysis/pmd/custom_analysis.xml -f text


ちょっとこれは「追記」になるのですが、
開発IDEEclipseIntelliJなど)のPMDプラグインを試してみたのですが、 「カスタム設定XMLを指定してそれを読み込む」というような機能が見あたらず、 それだとチームのメンバーがそれぞれ「手動でチェックして設定を合わせる」 というような形になってしまうので、あまりしっくり来ないというかそれだと意味ないな、 と感じられ、
私たちのプロジェクトでは現在でも上記のようなコマンド・ベースで使っています。

と、ここまでは、
自分のPCの内部でローカル環境で実行しているイメージですが、
ほぼそのまま、インストールから実行まで、
CIサーバーのLinuxで同じことが出来ると思います。
(先ほどの、PMD本家サイトの「QuickStart」の「Linux」のタブの内容も参考にしてください。)

CIサーバーへのインストールはこんな感じ。

cd <PMDインストールフォルダ>
$ wget https://github.com/pmd/pmd/releases/download/pmd_releases%2F6.12.0/pmd-bin-6.12.0.zip
$ unzip pmd-bin-6.12.0.zip

CIサーバーでのPMDの実行はこんな感じ。

sh <PMDインストールフォルダ>/pmd-bin-6.12.0/bin/run.sh pmd -no-cache -d <プロジェクトのソースフォルダ> -R <プロジェクトのソースフォルダ>/static-code-analysis/pmd/custom_analysis.xml -f text || exit 0

このシェルを、例えば、Jenkins(やそれに類するもの)などでキックすることで、 テキストとしての解析結果を出力することになるかと思います。

PMDの「カテゴリーと優先度」について

さて、少しだけ話が変わって、
PMDのよい点として、
それぞれのチェック項目が、
Javaの1個のルール・クラスに対応していることです。

また、PMDはオープンソースであり、
オープンソース・コミュニティによって、
そのチェック項目(Javaクラス)がメンテナンスされています。

pmd.github.io

上に掲げたドキュメントを見てもらうと、分かるのですが、
それぞれのチェック項目には
■カテゴリーと優先度
という分類があります。

例えば、さっきの
「不要なimport文があるかどうか」のチェックは、
UnusedImportsRulesというJavaのクラスに対応しており、
このチェック項目については、
「カテゴリー」は「bestpractices」であり、
「優先度」は「Medium Low (4)」となります。

Best Practices | PMD Source Code Analyzer

pmd/UnusedImportsRule.java at master · pmd/pmd · GitHub


そこで、
このドキュメントを読みながら、
試しにチェック項目を追加してみて、
実際に解析結果がどのようになるかを見てみることで、
どのようなチェック項目を最終的に入れるべきかを検討して行きました。


ここからは、私たちのプロジェクトで、
■PMD導入にあたっての考え方・ポリシー
みたいなものをまとめてみたので、以下に紹介しておきます。


●方針1
まず、各カテゴリーに関して、
Priority: High (1)
のものを重視します。

もし、Priority: High (1) のチェックを無視する場合には、
明示的にコメントアウトの形で残しておき、なぜ無視しているのかの理由も合わせて書いておきます。

一方で、Priority: High (1) 以外でも、
Priority: Medium High (2) Priority: Medium (3) などで、これは有用だと思ったものは加えています。

●方針2
次に、以下のカテゴリーを重視します。
errorprone
multithreading
security

「errorprone」は辞書で引くと「エラーを引き起こしやすい」という意味です。

errorproneについては Priority: High (1) の項目に加えて、rulesets/java/basic.xml の errorprone にあるもの全てを加えてあります。
multithreadingについては、UseConcurrentHashMapを除いて、全項目を加えてあります。
securityについては、親のsecurity.xmlだけを指定しているので、いつも全項目がチェックされるようにしてあります。(現状では2項目しかありませんが。)

●方針3
次に、bestpracticesのカテゴリーを重視します。
ここには、Priority: High (1) の項目はないものの、良いチェック項目があると思います。

例えば、「Effective Java」に昔からある、
「戻りの型がListやMapだったらnullを返すのではなく空のListやMapを返しましょう」
のようなことをチェックしてくれる項目もあります。

●方針4
designとperformanceのカテゴリーは、Priority: High (1) の項目だけに留めています。

●方針5
最後の順番で、codestyleのカテゴリーです。
ここに、Priority: High (1) の項目があるものの、自分たちのプロジェクトに合わないと感じたものはコメントアウトしてあります。
※例としては、MyBatisGeneratorの自動生成コードがチェックされて怒られるなど。

★方針の補足
documentationのカテゴリーについては、Priority: High (1) の項目がないものの、必要とあれば後で追加して行きます。


以上までが、
今回のPMD導入にあたっての、
考え方・ポリシー
となります。

カスタム設定XMLの例

最後になりますが、
■カスタム設定XMLの例
という意味合いで、私たちのプロジェクトのカスタム設定XMLを載せておきます。

(※先ほどのポリシー・方針の内容も、
プロジェクトのソース・コードの一部分として引き継いで行けるように、
XMLファイルにそのままコメントとして入れてあります。)

<?xml version="1.0"?>
<ruleset name="customruleset">

    <description>
    static code analysis for XXXX Java source code by PMD
    </description>

    <!-- https://pmd.github.io/ -->
    <!-- https://pmd.github.io/pmd-6.10.0/pmd_rules_java.html -->

    <!--
    以下、基本的な方針を書いておきます。
    まず、各カテゴリーに関して、
    Priority: High (1)
    のものを重視します。
    もし、Priority: High (1) のチェックを無視する場合には、
    明示的にコメントアウトの形で残しておき、なぜ無視しているのかの理由も合わせて書いておきます。
    一方で、Priority: High (1) 以外でも、
    Priority: Medium High (2) Priority: Medium (3) などで、これは有用だと思ったものは加えています。
    次に、以下のカテゴリーを重視します。
    errorprone
    multithreading
    security
    errorproneについては Priority: High (1) の項目に加えて、rulesets/java/basic.xml の errorprone にあるもの全てを加えてあります。
    multithreadingについては、UseConcurrentHashMapを除いて、全項目を加えてあります。
    securityについては、親のsecurity.xmlだけを指定しているので、いつも全項目がチェックされるようにしてあります。(現状では2項目しかありませんが。)
    次に、bestpracticesのカテゴリーを重視します。
    ここには、Priority: High (1) の項目はないものの、良いチェック項目があると思います。
    designとperformanceのカテゴリーは、Priority: High (1) の項目だけに留めています。
    最後の順番で、codestyleのカテゴリーです。
    ここに、Priority: High (1) の項目があるものの、自分たちのプロジェクトに合わないと感じたものはコメントアウトしてあります。
    ※例としては、MyBatisGeneratorの自動生成コードがチェックされて怒られるなど。
    補足:documentationのカテゴリーについては、Priority: High (1) の項目がないものの、必要とあれば後で追加して行きます。
    -->

    <!-- bestpractices ここから -->

    <!-- このカテゴリーは Priority: High (1) の項目がないので気に入ったものを少しづつ追加して行く -->
    <!-- ↓これはMyBatisGeneratorの自動生成コードが怒られる・・・ -->
    <!-- <rule ref="category/java/bestpractices.xml/AbstractClassWithoutAbstractMethod" /> -->
    <rule ref="category/java/bestpractices.xml/AccessorClassGeneration" />
    <rule ref="category/java/bestpractices.xml/AccessorMethodGeneration" />
    <rule ref="category/java/bestpractices.xml/ArrayIsStoredDirectly" />
    <rule ref="category/java/bestpractices.xml/AvoidPrintStackTrace" />
    <rule ref="category/java/bestpractices.xml/AvoidReassigningParameters" />
    <rule ref="category/java/bestpractices.xml/AvoidStringBufferField" />
    <!-- ↓これは使わないと思うが念のため -->
    <rule ref="category/java/bestpractices.xml/CheckResultSet" />
    <rule ref="category/java/bestpractices.xml/ConstantsInInterface" />
    <rule ref="category/java/bestpractices.xml/DefaultLabelNotLastInSwitchStmt" />
    <rule ref="category/java/bestpractices.xml/ForLoopCanBeForeach" />
    <rule ref="category/java/bestpractices.xml/UnusedFormalParameter" />
    <!-- ↓これはやり過ぎかな -->
    <!-- <rule ref="category/java/bestpractices.xml/GuardLogStatement" /> -->
    <!-- ↓これもいいけどちょっとやり過ぎかな -->
    <!-- <rule ref="category/java/bestpractices.xml/LooseCoupling" /> -->
    <rule ref="category/java/bestpractices.xml/MethodReturnsInternalArray" />
    <rule ref="category/java/bestpractices.xml/MissingOverride" />
    <!-- ↓これもいいけどちょっとやり過ぎかな -->
    <!-- <rule ref="category/java/bestpractices.xml/OneDeclarationPerLine" /> -->
    <rule ref="category/java/bestpractices.xml/PositionLiteralsFirstInCaseInsensitiveComparisons" />
    <rule ref="category/java/bestpractices.xml/PositionLiteralsFirstInComparisons" />
    <rule ref="category/java/bestpractices.xml/PreserveStackTrace" />
    <rule ref="category/java/bestpractices.xml/ReplaceEnumerationWithIterator" />
    <rule ref="category/java/bestpractices.xml/ReplaceHashtableWithMap" />
    <rule ref="category/java/bestpractices.xml/ReplaceVectorWithList" />
    <rule ref="category/java/bestpractices.xml/SwitchStmtsShouldHaveDefault" />
    <rule ref="category/java/bestpractices.xml/SystemPrintln" />
    <rule ref="category/java/bestpractices.xml/UnusedFormalParameter" />
    <rule ref="category/java/bestpractices.xml/UnusedImports" />
    <rule ref="category/java/bestpractices.xml/UnusedLocalVariable" />
    <!-- ↓以下の2つはSpringと相性が良くないので -->
    <!-- <rule ref="category/java/bestpractices.xml/UnusedPrivateField" /> -->
    <!-- <rule ref="category/java/bestpractices.xml/UnusedPrivateMethod" /> -->
    <!-- ↓これはMyBatisGeneratorの自動生成コードが怒られる・・・ -->
    <!-- <rule ref="category/java/bestpractices.xml/UseCollectionIsEmpty" /> -->
    <!-- ↓言っていることは分かるが、チェックまでは要らないかな、それぞれの場面で判断すればいいと思う。 -->
    <!-- <rule ref="category/java/bestpractices.xml/UseVarargs" /> -->

    <!-- bestpractices ここまで -->

    <!-- codestyle ここから -->

    <!-- 以下が Priority: High (1) のもの全て -->
    <!-- ↓これは厳し過ぎる!人間によるレビュー・チェックでいいと思う -->
    <!-- <rule ref="category/java/codestyle.xml/ClassNamingConventions" /> -->
    <rule ref="category/java/codestyle.xml/EmptyMethodInAbstractClassShouldBeAbstract" />
    <!-- ↓これも厳し過ぎる!人間によるレビュー・チェックでいいと思う -->
    <!-- <rule ref="category/java/codestyle.xml/FieldNamingConventions" /> -->
    <!-- ↓これも厳し過ぎる!人間によるレビュー・チェックでいいと思う -->
    <!-- <rule ref="category/java/codestyle.xml/FormalParameterNamingConventions" /> -->
    <!-- ↓これも厳し過ぎる!人間によるレビュー・チェックでいいと思う -->
    <!-- <rule ref="category/java/codestyle.xml/LocalVariableNamingConventions" /> -->
    <!-- ↓単体テスト系のメソッド名は全て出力される、逆に言うと出るのはそれだけ、これはtest以外で走らせる? -->
    <!-- <rule ref="category/java/codestyle.xml/MethodNamingConventions" /> -->
    <!-- ↓これも厳し過ぎる!人間によるレビュー・チェックでいいと思う -->
    <!-- <rule ref="category/java/codestyle.xml/VariableNamingConventions" /> -->

    <!-- codestyle ここまで -->

    <!-- design ここから -->

    <!-- 以下が Priority: High (1) のもの全て -->
    <rule ref="category/java/design.xml/AbstractClassWithoutAnyMethod" />
    <rule ref="category/java/design.xml/AvoidThrowingNullPointerException" />
    <!-- ↓これはどうかな!?MyBatisGeneratorの自動生成コードも怒られるし・・・ -->
    <!-- <rule ref="category/java/design.xml/AvoidThrowingRawExceptionTypes" /> -->
    <!-- ↓なるほど言っていることは分かる、しかしこれはどうかな? -->
    <!-- <rule ref="category/java/design.xml/ClassWithOnlyPrivateConstructorsShouldBeFinal" /> -->

    <!-- design ここまで -->

    <!-- documentation -->
    <!-- このカテゴリーは Priority: High (1) の項目がないものの必要とあれば後で追加します -->

    <!-- errorprone ここから -->

    <!-- まずは Priority: High (1) のもの全て -->
    <rule ref="category/java/errorprone.xml/ConstructorCallsOverridableMethod" />
    <rule ref="category/java/errorprone.xml/EqualsNull" />
    <rule ref="category/java/errorprone.xml/ReturnEmptyArrayRatherThanNull" />

    <!-- 以下は rulesets/java/basic.xml の中の errorprone カテゴリーにあるものを全て追加しました -->
    <!-- https://github.com/pmd/pmd/blob/master/pmd-java/src/main/resources/rulesets/java/basic.xml -->
    <rule ref="category/java/errorprone.xml/AvoidBranchingStatementAsLastInLoop" />
    <rule ref="category/java/errorprone.xml/AvoidDecimalLiteralsInBigDecimalConstructor" />
    <rule ref="category/java/errorprone.xml/AvoidMultipleUnaryOperators" />
    <rule ref="category/java/errorprone.xml/AvoidUsingOctalValues" />
    <rule ref="category/java/errorprone.xml/BrokenNullCheck" />
    <rule ref="category/java/errorprone.xml/CheckSkipResult" />
    <rule ref="category/java/errorprone.xml/ClassCastExceptionWithToArray" />
    <rule ref="category/java/errorprone.xml/DontUseFloatTypeForLoopIndices" />
    <rule ref="category/java/errorprone.xml/JumbledIncrementer" />
    <rule ref="category/java/errorprone.xml/MisplacedNullCheck" />
    <rule ref="category/java/errorprone.xml/OverrideBothEqualsAndHashcode" />
    <rule ref="category/java/errorprone.xml/ReturnFromFinallyBlock" />
    <rule ref="category/java/errorprone.xml/UnconditionalIfStatement" />

    <!-- errorprone ここまで -->

    <!-- multithreading ここから -->

    <!-- このカテゴリーはほぼ全ての項目を追加するようにします -->
    <rule ref="category/java/multithreading.xml/AvoidSynchronizedAtMethodLevel" />
    <rule ref="category/java/multithreading.xml/AvoidThreadGroup" />
    <rule ref="category/java/multithreading.xml/AvoidUsingVolatile" />
    <rule ref="category/java/multithreading.xml/DoNotUseThreads" />
    <rule ref="category/java/multithreading.xml/DontCallThreadRun" />
    <rule ref="category/java/multithreading.xml/DoubleCheckedLocking" />
    <rule ref="category/java/multithreading.xml/NonThreadSafeSingleton" />
    <rule ref="category/java/multithreading.xml/UnsynchronizedStaticDateFormatter" />
    <!-- クラスのフィールドなどでないローカル・スコープのHashMapまで全てConcurrentHashMapに置き換えろ、というのはやり過ぎのように思う。 -->
    <!-- <rule ref="category/java/multithreading.xml/UseConcurrentHashMap" /> -->
    <rule ref="category/java/multithreading.xml/UseNotifyAllInsteadOfNotify" />

    <!-- multithreading ここまで -->

    <!-- performance ここから -->

    <!-- 以下が Priority: High (1) のもの全て -->
    <rule ref="category/java/performance.xml/AvoidFileStream" />
    <!-- ↓これもやり過ぎかなと思う。 -->
    <!-- この設定を有効にすると、coreのtestのTestValueクラスのshort型のフィールドに警告を出すが、その1個所だけである。 -->
    <!-- <rule ref="category/java/performance.xml/AvoidUsingShortType" /> -->

    <!-- performance ここまで -->

    <!-- security -->
    <!-- このカテゴリーはsecurity.xmlだけを指定していつも全ての項目がチェックされるようにしておきます -->
    <rule ref="category/java/security.xml" />

</ruleset>


このカスタム設定XMLですが、
「これが最高だ!」という意味合いではまったくなく、
私たちのプロジェクトとはぜんぜん違う性質を持ったプロジェクトの場合には、 また違った設定が合っているでしょう。

これからも、このカスタム設定XMLは、時と共に、育てて行こうと思っています。

導入してよかったと思う点

静的解析ツールを導入したからといって、
「だからすぐに品質が上がる」というものではないと思います。

「自分たちのプロジェクトの基準を、自分たちのコード・ベースで管理して、それを次に続く人たちに引き継いで行ける。」

「自分たちのプロジェクトのその基準に合致しているかどうかは、人間の目によるチェックだけではなく、機械のチェックに助けてもらう。」

という部分に価値があるのかなと思っています。

Java × Spring Boot のマルチプロジェクトでのMyBatisの単体テストについて

KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。  本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。

はじめに

こんにちは、KCFのエンジニアの坂本です。

私たちの開発チームでは、Java × Spring Boot でORマッパーに MyBatis 3 を採用しています。

今日は、そのMyBatisを使ってる場合の単体テストについて書いてみたいと思います。

背景について

自分たちのチームでは、
システム全体の保守性を上げるために(相互依存性を下げるために)
以下の構成を採用しています。

●マルチプロジェクト構成
●データベースにアクセスするロジックなどはcoreというサブプロジェクトへ集める

うーん、ブログって、こういう背景というか、そもそもの前提を書く部分って、なかなか難しいですよね。
※まあ、そのまま進めます。

「そのcoreプロジェクトについて単体テストする場合はMockじゃなくて本当のデータを入れてテストしたいよね」

ということで、
単体テストのポリシーは、色いろな方法・アプローチがあると思いますが、 自分たちのチームでは「こうやってるよ」という「例」として、書いてみたいと思います。

build.gradleやH2とFlywayの設定など

これから、順を追って説明して行きますが、
もしかすると最後に書いてある「さて本題の単体テスト」をまず読んでから、逆に追って読んだ方が分かりやすいかもしれません。
※とはいえ、このまま進めます。

まず、自分たちのチームでの、
●マルチプロジェクト構成
のbuild.gradleとsettings.gradleは
以下のような感じになってます。

■build.gradle

buildscript {
    ext {
        springBootVersion = "2.0.1.RELEASE"
    }
    repositories {
        mavenCentral()
        maven {
            url("https://plugins.gradle.org/m2/")
        }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("gradle.plugin.com.arenagod.gradle:mybatis-generator-plugin:1.4")
    }
}

subprojects {
    apply plugin: "java"
    apply plugin: "eclipse"
    apply plugin: "org.springframework.boot"
    apply plugin: "io.spring.dependency-management"
    sourceCompatibility = 1.8
    repositories {
        mavenCentral()
        maven {
            url("https://repo.spring.io/libs-snapshot")
            url("http://www.datanucleus.org/downloads/maven2/")
        }
    }
    dependencies {
        // ここにプロジェクトに応じて様ざまな依存関係が入ると思われます
        compileOnly("org.projectlombok:lombok")
    }
}

project(":datasource") {
    apply plugin: "com.arenagod.gradle.MybatisGenerator"
    dependencies {
        compile("org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.2")
    }
    configurations {
        mybatisGenerator
    }
    mybatisGenerator {
        verbose = true
        configFile = "${projectDir}/src/main/resources/mybatis/generatorConfig.xml"
    }
    jar {
        baseName = "datasource"
        version = "1.0.0"
        exclude("mybatis/**")
        enabled = true
    }
    bootJar.enabled = false
}

project(":core") {
    dependencies {
        compile project(":datasource")
        compile("org.springframework.boot:spring-boot-starter-aop")
        compile("oracle:ojdbcXX:YYYY")
        // ここにプロジェクトに応じて様ざまな依存関係が入ると思われます
        testCompile("org.mybatis.spring.boot:mybatis-spring-boot-starter-test:1.3.2")
        testCompile("org.flywaydb:flyway-core")
        testRuntime("com.h2database:h2")
     }
    jar {
        baseName = "core"
        version = "1.0.0"
        enabled = true
    }
    bootJar.enabled = false
}

project(":web") {
    dependencies {
        compile project(":core")
        compile("org.springframework.boot:spring-boot-starter-actuator")
        compile("org.springframework.boot:spring-boot-starter-thymeleaf")
        testCompile("org.springframework.boot:spring-boot-starter-test")
    }
    bootJar.enabled = true
    bootJar {
        launchScript()
    }
}

■settings.gradle

include 'datasource', 'core', 'web'


そろそろ、単体テストの話に入って行きますね。
以下が、coreプロジェクトのtestのresourceファルダに置いている設定ファイルです。

■core/src/test/resources/application-test.yml

spring:
  profiles:
    active: test

---

spring:
  profiles: test

  datasource:
    url: jdbc:h2:mem:sampledb;MODE=Oracle;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;AUTOCOMMIT=OFF
    username: sa
    password:
    driver-class-name: org.h2.Driver

私たちのチームでは、単体テストに使うデータベースは専用でインメモリの
■H2
を使ってます。

インメモリのH2であれば、別の単体テスト用のデータベースを立てる必要がないので便利ですね!

www.h2database.com

H2は起動時に動作モードをMySQLPostgreSQLOracleなどで指定してエミュレートの指定が出来ます。


また単体テスト実行時のスキーマやテスト・データの管理は
■Flyway
に任せています。

始めに載せた
build.gradleにあったように
testの依存関係にFlywayを入れておけば、
あとはSpring BootのAutoConfigurationが「よしな」にやってくれます。

以下は、Flyway用のスキーマ・ファイルの「例」です。


V1_create_table.sql

/* SAMPLE_DATAテーブル */

drop table if exists SAMPLE_DATA;

create table "SAMPLE_DATA"
 ("ID" NUMBER(3,0) NOT NULL ENABLE,
  "NAME" VARCHAR2(10) NOT NULL ENABLE,
  CONSTRAINT "SAMPLE_DATA_PK" PRIMARY KEY ("ID")
 );

V2_insert_test_data.sql

/* SAMPLE_DATAテスト・データ */

insert into SAMPLE_DATA (ID, NAME) values (1, 'name1');

@SpringBootTestとConfigurationクラス

単体テスト・クラスには@SpringBootTestアノテーションを付けているのですが、 そこに合わせて、下記のようなConfigurationクラスを指定しています。

@SpringBootTest(classes = {TestConfiguration.class, TestDataSourceConfiguration.class})

以下は、この2個のConfigurationクラスの説明です。

■TestConfigurationクラス

マルチプロジェクト構成の中でcore(とcoreに依存関係のあるサブプロジェクト)を読み込むよ
というComponentScanの設定をしています。

package jp.samplesample.application.core.configuration;

import static org.mockito.Mockito.mock;

import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@ComponentScan(basePackages = "jp.samplesample.application.core")
@Profile("test")
public class TestConfiguration {

    // ここにMockのBeanが必要な場合もあると思います

    @Bean
    public XXX xxx() {
        return mock(XXX.class);
    }

}


■TestDataSourceConfigurationクラス

こちらはMyBatisの自動生成ファイルのあるdatasourceサブプロジェクトを読み込むMapperScanの設定と
上で既に挙げていたapplication-test.ymlの読み込みなどを指定してます。

package jp.samplesample.application.core.configuration;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import jp.samplesample.application.core.constant.TestValue;
import jp.samplesample.application.datasource.mapper.XXXCustomMapper;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
@MapperScan("jp.samplesample.application.datasource.mapper")
@TestPropertySource(locations = "classpath:application-test.yml")
@Profile("test")
public class TestDataSourceConfiguration {

    // 本体のデータベースでは通るがH2では通らないようなSQLを持っている場合には
    // 以下のようにMockのBeanを入れる場合もあるでしょう

    @Bean
    public XXXMapper xxxMapper() {
        XXXMapper xxxMapper = mock(XXXMapper.class);
        // ここにMockの振る舞いを仕込んでおく
        return xxxMapper;
    }

}

coreサブプロジェクトのServiceとRepository

本題の単体テストの例を載せた時に、
具体的に分かりやすいようにするため、
単体テストの対象となるServiceクラス
(と合わせてServiceクラスを構成している要素)
のサンプルみたいなものも書いておきます。


■SampleDomainクラス

package jp.samplesample.application.core.domain;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class SampleDomain {

    private Integer id;

    private String name;

}


■SampleDomainRepositoryクラス(インターフェース)

package jp.samplesample.application.core.repository;

import jp.samplesample.application.core.domain.SampleDomain;
import org.springframework.stereotype.Repository;

@Repository
public interface SampleDomainRepository {

    SampleDomain findOne(Integer id);

    SampleDomain save(SampleDomain sampleDomain);

}

※「Repository」は「DAO」と呼ぶ人・プロジェクトもあるかもしれないですね


以下が、SampleDomainRepositoryインターフェースの実装クラス(の枠・概要)です。

■SampleDomainRepositoryImplクラス

package jp.samplesample.application.core.repository.impl;

import jp.samplesample.application.core.domain.SampleDomain;
import jp.samplesample.application.core.repository.SampleDomainRepository;
import jp.samplesample.application.datasource.mapper.SampleDataMapper;
import jp.samplesample.application.datasource.model.SampleDataExample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import lombok.NonNull;

@Repository
public class SampleDomainRepositoryImpl implements SampleDomainRepository {

    @Autowired
    private SapmleDataMapper sampleDataMapper;

    @Override
    public SampleDomain findOne(@NonNull Integer id) {
        // ここにMyBatisのMapperとModelを使って実装を書く
    }

    @Override
    public SampleDomain save(@NonNull SampleDomain sampleDomain) {
        // ここにMyBatisのMapperとModelを使って実装を書く
    }

}


やっと今回の単体テストの対象である
SampleDomainServiceクラス
に辿り着きました!


■SampleDomainServiceクラス

package jp.samplesample.application.core.service;

import jp.samplesample.application.core.SampleDomain;
import jp.samplesample.application.core.repository.SampleDomainRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigInteger;

import lombok.NonNull;

@Service
public class SampleDomainService {

    @Autowired
    private SampleDomainRepository sampleDomainRepository;

    @Transactional(readOnly = true)
    public SampleDomain get(@NonNull Long id) {
        return sampleDomainRepository.findOne(id);
    }

    @Transactional(readOnly = false)
    public SampleDomain save(@NonNull SampleDomain sampleDomain) {
        // 例えば以下のようなsaveのための何らかの条件が書いてある(これはちょっとひどい例だけど)
        if (sampleDomain.getId() <= 2) {
            return sampleDomainRepository.save(sampleDomain);
        } else {
            return null;
        }
    }

}

さて本題の単体テスト

coreサブプロジェクトのSampleDomainServiceクラスの
getメソッドとsaveメソッド
が今回の単体テストの対象です。

package jp.samplesample.application.core.service;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;

import jp.samplesample.application.core.configuration.TestConfiguration;
import jp.samplesample.application.core.configuration.TestDataSourceConfiguration;

import jp.samplesample.application.core.domain.SampleDomain;
import jp.samplesample.application.core.repository.SampleDomainRepository;
import jp.samplesample.application.core.service.SampleDomainService;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {TestConfiguration.class, TestDataSourceConfiguration.class})
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("test")
public class SampleServiceTest {

    @Autowired
    private SampleDomainService sampleDomainService;

    @Autowired
    private SampleDomainRepository sampleDomainRepository;

    @Test
    public void test_get_期待通りデータが取得できること() {
        Integer id = 1;
        String name = "name1";

        // テスト対象メソッドの実行
        SampleDomain sampleDomain = sampleDomainService.get(id);

        // 結果検証
        assertThat(sampleDomain.getId(), is(id));
        assertThat(sampleDomain.getName(), is(name));
    }

    @Test
    public void test_save_IDが2の場合はデータが作成できること() {
        Integer id = 2;
        String name = "name2";

        SampleDomain saveSampleDomain = new SampleDomain();
        saveSampleDomain.setId(id);
        saveSampleDomain.setId(name);

        // テスト対象メソッドの実行
        sampleDomainService.save(saveSampleDomain);

        // 検証対象データの取得
        SampleDomain resultSampleDomain = sampleDomainRepository.findOne(id);

        // 結果検証
        assertThat(resultSampleDomain.getId(), is(id));
        assertThat(resultSampleDomain.getName(), is(name));
    }

    @Test
    public void test_save_IDが3の場合はデータが作成されないこと() {
        Integer id = 3;
        String name = "name3";

        SampleDomain saveSampleDomain = new SampleDomain();
        saveSampleDomain.setId(id);
        saveSampleDomain.setId(name);

        // テスト対象メソッドの実行
        sampleDomainService.save(saveSampleDomain);

        // 検証対象データの取得
        SampleDomain resultSampleDomain = sampleDomainRepository.findOne(id);

        // 結果検証
        assertThat(resultSampleDomain, is(nullValue()));
    }

}


こんなに単純な例だと、
あまり「ありがたみ」が感じられないかもしれませんが、
もっと複雑になる実際のアプリケーションでは、例えば、
「ある条件のもとでデータベースに入るデータの更新時刻を単体テストでチェックしておく」など、
色いろと助かることがあると思います。

つまづいた点(指定のH2が立ちあがらない場合)

上記の単体テストの「例」ですが、
特に以下のアノテーションが重要でした。
これを付けないと、
application-test.ymlで指定したH2が、
立ちあがらないという現象が起こったはずです。

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

Robot Framework、RESTinstanceでWebAPIのテストをする

KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。  本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。

はじめまして。エンジニアの河野です。

最近、担当しているプロダクトのリグレッションテストを実施しようとしていて、何か良いツールないかと探した結果、Robot Frameworkを使ってみることにしました。

やりたいことはとりあえずWeb APIのシナリオテスト的なものを実施したかったので、RESTinstanceというプラグインを使うと記述が楽そうだったのでこちらも一緒に利用してみました。

そちらも絡めて軽くレポートさせて頂きます。

インストール手順

基本的に公式サイトにドキュメントへのリンクがまとまっているので、インストールからデモ動作まではこちらから各種ドキュメント見て頂くのが良いと思いますが、ざっと説明しておきます。

robotframework.org

github.com

今回はpipでインストールするのでpipは必須となります。

自分はDocker上のUbuntuコンテナに入れたので、以下をDockerfileに記述しておきました。

Robot Framework自体はプロセスではないのですが、ローカルマシンにあれこれ入れたくないのとローカル用の開発環境もDokcerなので検証しやすかったので。

日本語使いたかったのでlanguage-packも入れています。

  • Dockerfile
FROM ubuntu:18.04
MAINTAINER kcf.kono

ENV DEBIAN_FRONTEND=noninteractive

# install packages
RUN apt-get update
RUN apt-get install -y git python3-pip

# install additional packages
RUN apt-get install -y tzdata locales language-pack-ja-base language-pack-ja

# setting
RUN locale-gen ja_JP.UTF-8

RUN mkdir /sync

ENV LANG=ja_JP.UTF-8

# install robot framework
RUN pip3 install docutils robotframework

RUN pip3 install --upgrade RESTinstance

Robot FrameworkとRESTinstanceのインストール部分はこちらになります。

RUN pip3 install docutils robotframework

RUN pip3 install --upgrade RESTinstance

とりあえず動かしてみる

QuickStartGideがあるので、そちらをGitHubから持ってきて動作させてみます。

$ git clone https://github.com/robotframework/QuickStartGuide.git

QuickStart.rstというreStructuredText形式のファイルがあって読んでみると、直接そのファイルを指定して実行することが出来る様です。

ただ、拡張子が.robot以外のファイルは非推奨と警告でるので、自分でテストケース作る場合はrobotファイルを作ってそちらに記述してます。

$ robot QuickStart.rst

[ WARN ] Automatically parsing other than '*.robot' files is deprecated. Convert '/sync/QuickStartGuide/QuickStart.rst' to '*.robot' format or use '--extension' to explicitly configure which files to parse.
==============================================================================
QuickStart
==============================================================================
User can create an account and log in                                 | PASS |
------------------------------------------------------------------------------
User cannot log in with bad password                                  | PASS |
------------------------------------------------------------------------------
User can change password                                              | PASS |
------------------------------------------------------------------------------
Invalid password                                                      | PASS |
------------------------------------------------------------------------------
User status is stored in database                                     | PASS |
------------------------------------------------------------------------------
QuickStart                                                            | PASS |
5 critical tests, 5 passed, 0 failed
5 tests total, 5 passed, 0 failed
==============================================================================
Output:  /sync/QuickStartGuide/output.xml
Log:     /sync/QuickStartGuide/log.html
Report:  /sync/QuickStartGuide/report.html

実行後にはxmlとhtmlが出力され、htmlファイルを開くと結果レポートが表示されます。

結果レポートのスクリーンショットこちらのEXAMPLE 2に載っているので参考になるかと思います。

テストケースを記述してみる

まずは簡単なテストケースを記述してみることにします。

今回はWeb APIのテストを行いたいので、RESTinstanceを利用してヘルスチェック用APIへのテストを実行してみようと思います。

ヘルスチェック用APIはHTTPステータス 200を返すだけのものとなります。

*** Settings ***
Library       REST    https://example.com

*** Test Cases ***
リクエストを送るとHTTP STATUS 200が返ってくる
    GET         /health_check
    Integer     response status     200

Robot FrameworkはPythonで出来ていて、テストケースの記述方法もPythonに習ってテーブル形式で記述します。

「*** Settings ***」テーブルにはインポートするライブラリを記述します。

今回はRESTinstanceを利用するのでRESTと指定しています。

RESTの後に接続するURLを指定が出来、アクション実行時にURLを省略することが出来ます。

指定しない場合はアクセスの都度指定することになります。

「***Test Cases ***」テーブルに実際のアクションを記述します。

行頭にインデント入れていない部分がテストケースのタイトルになります。

Python3使っているからか普通に日本語は使えました。

アクションは行頭にインデント入れて字下げした箇所に記述します。

GET [PATH]で、[PATH]に対してGETリクエストして

Integer [検証対象] [期待値] でHTTPステータスが200で返ってくることを検証しています。

実行すると以下の様になります。

==============================================================================
Health Check :: RESTinstanceのサンプル用にヘルスチェック用APIのリクエストを...
==============================================================================
[ WARN ] Response body content is not JSON. Content-Type is: text/plain; charset=utf-8
リクエストを送るとHTTP STATUS 200が返ってくる                         | PASS |
------------------------------------------------------------------------------
Health Check :: RESTinstanceのサンプル用にヘルスチェック用APIのリ...  | PASS |
1 critical test, 1 passed, 0 failed
1 test total, 1 passed, 0 failed
==============================================================================
Output:  /sync/tests/output.xml
Log:     /sync/tests/log.html
Report:  /sync/tests/report.html

ヘルスチェックのResponse Bodyが空文字列なので、Warning出ていますがとりあえずテストをパスしました。

RESTinstanceの良いところはHTTPリクエストのテストケースをシンプルに記述出来るところで、同じHTTPリクエスト系のライブラリのrobotframework-requestsを使うと以下の様に若干めんどくさくなります。

*** Settings ***
Documentation   robotframework-requestsのサンプル用にヘルスチェック用APIのリクエストを記述
Library         RequestsLibrary

*** Test Cases ***
リクエストを送るとHTTP STATUS 200が返ってくる
    Create Session                  sample                  https://example.com     verify=True
    ${resp}=                        Get Request             sample                  /health_check
    Should Be Equal As Strings      ${resp.status_code}     200

もう少し凝ったテストをしてみる

実際のシナリオテストとなるともう少し凝ったことをするケースがあるので、以下をやってみました。

  1. 認証用APIからトークンを取得
  2. 取得したトークンをヘッダにセット
  3. データ一覧取得用のAPIをリクエス
  4. レスポンスデータのJSONの特定のカラムを検証

これをテストケースとして記述してみます。

*** Settings ***
Library     REST

*** Variables ***
${api_url}       https://example.com
${auth_url}      https://auth.example.com
${auth_api_key}  api_key_1
${auth_header}   {"x-api-key": "${auth_api_key}"}
${auth_json}     {"id": 1 , "credential_key": "qazxswedcvfr"}

*** Test Cases ***
ログインしてデータ一覧取得
    Set headers             ${auth_header}
    &{auth_response}        POST                                               ${auth_url}/loginauth     ${auth_json}
    Output                  ${auth_response.body.token}
    Set headers             {"x-auth-token": "${auth_response.body.token}"}
    &{response}             GET                                                ${api_url}/resources
    Integer                 response status                                    200
    String                  ${response.body[0].name}                           "テストデータ"

上記のテストケースですが、新たに「*** Variables ***」というテーブルを追加しています。

こちらでは上記の様に変数を定義することが出来ます。

また、「*** Test Cases ***」テーブルの中でも結果を変数に代入することが可能です。

これでリクエストの結果を利用して他のリクエストを実行することなどが出来ます。

変数の宣言方法は以下があるようです。

RESTinstanceのサンプルから引用します。

    ${json}=      {"foo": "bar" }   # JSON object, represented as Python str
    &{dict}=      foo=bar           # Python dict, corresponds to JSON object
    ${array}=     ["foo", "bar"]    # JSON array, represented as Python str
    @{list}=      foo   bar         # Python list, corresponds to JSON array

${VARIABLE}で定義するとスカラ変数(String)となりますが、&{VARIABLE}で定義すると辞書変数(Object)になるのでJSON形式のレスポンスに${auth_response.body.token}のようにアクセスすることも出来ます。

RESTinstanceの場合は$自体がresponse bodyと等価の様なので、以下の様に記述することも可能です。

...省略
*** Test Cases ***
ログインしてデータ一覧取得
    Set headers             ${auth_header}
    &{auth_res}             POST                                               ${auth_url}/loginauth     ${auth_json}
    Output                  $.token
    Set headers             {"x-auth-token": "${auth_res.body.token}"}
    &{response}             GET                                                ${api_url}/resources
    Integer                 response status                                    200
    String                  $[0].name                                          "テストデータ"

外部ファイル読み込み

「*** Variables ***」テーブルの内容を別ファイルにし、以下のように外部ファイルをincludeすることも出来ます。

  • common.robot
*** Variables ***
${api_url}       https://example.com
${auth_url}      https://auth.example.com
${auth_api_key}  api_key_1
${auth_header}   {"x-api-key": "${auth_api_key}"}
${auth_json}     {"id": 1 , "credential_key": "qazxswedcvfr"}
  • sample.robot
*** Settings ***
Library     REST
Resource     common.robot
...省略

上記のように「*** Settings ***」テーブルでResource [PATH]とすることで外部ファイルを読み込む事ができるので、テストファイル毎に重複する様な場合に効率的です。

環境変数

環境変数も取り込むことが可能です。

リポジトリに上げたくない様な機密情報はOSの環境変数に設定して、参照することでテストケースへの記述を回避することが出来ます。

環境変数は%{VARIABLE}で参照出来ます。

設定例としては以下になります。

$ export AUTH_API_KEY=api_key_1

$ cat common.robot

*** Variables ***
${api_url}       https://example.com
${auth_url}      https://auth.example.com
${auth_api_key}  %{AUTH_API_KEY}
${auth_header}   {"x-api-key": "${auth_api_key}"}
${auth_json}     {"id": 1 , "credential_key": "qazxswedcvfr"}

変数についてはこちらにドキュメントがまとまっています。

日本語翻訳版があるのが英語の得意でない自分にはありがたいですね。

まとめ

今回は簡単なWeb APIのシナリオテストを作って実施してみました。

使ってみた感想としては以下になります。

良かったところ

  • DSLが直感的で分かりやすい
  • 機能が豊富なため細かい制御が可能
  • 出来るといいなが大抵用意されている
  • ドキュメントが充実している
  • RESTinstanceは記述を楽にしてくれる

良くなかったところ

  • テーブルのインデント調整がめんどくさい (面合わせなくてもいいのですが合わせないと見にくいので。フォーマッターで解決出来そうですが。)
  • RESTinstanceのスター数が少ないし、バージョンも2018/12/20の時点ではまだ1.0.0rc4

正直、今回くらいの利用ではあまり困ったところもなく、Web APIのテストをやる程度でしたら大分楽に記述出来ました。

Selenium使ってフロントエンド含めたテストをやると大変かもしれませんが、今回そこはスコープ外なので考慮していません。

もう少し本格的にプロダクトチーム内でRobot Frameworkの導入をしてみようと思いますので、また新たな発見などあったら紹介させて頂こうと思います。

JenkinsのフリースタイルジョブでAWS ECRにイメージ登録!

f:id:seri_wb:20181114205542p:plain

KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。  本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。

エンジニアの土田です。

今はシステムリプレイスからDMP構築に業務が変わり、データ収集に勤しんでいます。

JenkinsでのECRデプロイ

JenkinsでのECRデプロイ方法を調べていたところ、パイプラインジョブを使った記事はいくつか見つかったのですが、 昔ながらのフリースタイルジョブでの手順が見当たらなかったので、今回はその方法を記載します。

作業概要

実現のためには、以下を行う必要があります。

  • ECRにアクセスするためのアクセスキー発行
  • JenkinsサーバにDockerをインストール
  • JenkinsサーバにAWS CLIをインストール
  • Jenkinsに必要なプラグインのインストールと設定
  • Jenkinsでデプロイ用のジョブ作成

Jenkinsサーバの設定

まだ、JenkinsでDockerを使っていない場合は、JenkinsにDockerをインストールしてください。

インストールできたら、Jenkinsの実行ユーザ(通常はjenkins)をdockerグループに所属させます。

$ sudo usermod -a -G docker jenkins
$ sudo systemctl restart jenkins

これで、JenkinsからDockerが利用できるようになります。

次に、JenkinsジョブからAWS CLIを使うためのプロファイルを作成します。
AWS CLIのインストールは以下のリンク参照

sudo -u jenkins aws configure --profile プロファイル名

実行すると以下のように問われるので、適時入力してください。

AWS Access Key ID [None]: アクセスキー
AWS Secret Access Key [None]: シークレットアクセスキー
Default region name [None]: ap-northeast-1
Default output format [None]: json

これで、サーバ側での設定は終了です。

Jenkinsの設定

ここからはJenkinsのWeb画面の設定になります。

JenkinsからECRにイメージ登録するためには、以下のプラグインが必要になるので、こちらをインストールします。

Dockerプラグインの設定

Jenkinsの管理からシステムの設定を開き、『クラウド』の項目でDockerを追加し、設定をしてきます。

f:id:seri_wb:20181114204521p:plain:w600

といってもName以外はDocker Host URIが最低限設定できていれば問題ありません。

設定値はCentOSであればunix:///var/run/docker.sockになると思います。

認証情報の設定

ECRにイメージ登録をするためのアクセスキーの情報を、Jenkinsの認証情報として登録します。

認証情報を登録する際は、認証情報をジョブから利用できるスコープが選択できます。 フォルダから認証情報のページに遷移すると、『Stores scoped to フォルダ名』のような項目があるので、 スコープ設定をしたい場合は、そこから遷移した先で認証情報を追加すると良いです。

認証情報の追加で、AWS Credentialsを選択し、先程のアクセスキー情報を設定してください。

f:id:seri_wb:20181114204507p:plain:w600

フリースタイルジョブの作成

いよいよジョブ作成です。

まずはソースコード管理に、作成するコンテナイメージを作るDockerfileが記載されたリポジトリを指定します。

次に、ビルド項目の『ビルド手順の追加』からBuild / Publish Docker Imageを選択します。

f:id:seri_wb:20181114204439p:plain:h300

『Build / Publish Docker Image』は以下のようになっているので、高度な設定ボタンを押下し、すべての項目を表示させてください。

f:id:seri_wb:20181114204450p:plain:w600

各項目に設定する値は以下のようになります。

項目
Directory for Dockerfile コードリポジトリのルートからたどってDockerfileのある場所
Docker registry URL https://ECRリポジトリのURI
Registry credentials ECRリポジトリの存在するリージョンの入ったキー情報
Cloud システム設定のクラウドで追加したDocker設定の名前
Image ECRのプッシュコマンドで指定されるタグ名
Push image チェックする
Registry Credentials 上記のクレデンシャルと同じ

これらの値を設定した画面が以下です。

f:id:seri_wb:20181114204535j:plain:w600

これで設定は完了です。

あとはこのジョブを実行すると、コンテナイメージが作成され、そのイメージがECRへ登録されます。


ECRのイメージへ独自タグの追加

コンテナイメージの運用をしていると独自のタグが必要になると思いますが、 今のところECRのコンソールからタグ追加はできないので、これもついでにJenkinsで行ってしまいましょう。

Jenkinsのシェル実行で、以下のようなタグ追加のCLIコマンドを記載してください。

$(aws ecr get-login --no-include-email --region ap-northeast-1 --profile 作成したプロファイル名)
 
MANIFEST=$(aws ecr batch-get-image --repository-name ECRのリポジトリ名 --image-ids imageTag=latest --query images[].imageManifest --output text --profile 作成したプロファイル名)
aws ecr put-image --repository-name ECRのリポジトリ名 --image-tag 付与するタグ名 --profile 作成したプロファイル名 --image-manifest "$MANIFEST"

必要なのはこれだけです。

まとめ

冒頭に書いたように、パイプラインを使って行う記事はあったのですが、昔ながらのやり方でやる方法がなかったのでまとめてみました。
Jenkinsはパイプラインが出てきてからGUIを使わないやり方が推されている感じがしますが、昔ながらのやり方もわかりやすくて良いと思うので、 状況にあわせて使っていきたいです。

ちなみにこれらの内容はAWS Batchで使うイメージのプッシュと、本番環境で参照するイメージ切り替えのためのタグ追加に利用しています。 プッシュして、開発でテストが終わったら本番用のタグを付与、といった感じで運用しています。

参考

Serverless Frameworkでローカル環境のセットアップからAWSへDeployまでを試してみる

KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。  本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。

KDDIコマースフォワード(以下KCF)モール開発部に所属するフロントエンドエンジニアの早川です。

早速ですが
みなさんSPA(Single Page Application)の開発行ってますか?

KCFでは一部のプロダクトや社内ツールで
SPAを採用し開発を行っています。

SPAによるユーザーへ提供できるメリットとして

  • 快適なUI操作(アクションからのレスポンスの速さやサクサクした画面切り替え)
  • 通信データ量の削減(必要なデータのみを取得することが可能)

などが考えられユーザー体験を向上させることができます。
そのためSPAを採用したコンテンツの開発を加速して行なっていきたいと考えています。

そうなるとサーバ側のAPIの開発についても
合わせて加速させていくことが重要になってきます。

みなさんはAPIの開発ってどのように行ってますか?

  • バックエンドエンジニアへ依頼する
  • 自分で開発する

バックエンドエンジニアへ依頼する形がセオリーかもしれませんが
手っ取り早く開発を進めるには 自分で作ってしまうのも1つの解決手段であると考えています。

そこで今回はフロントエンドエンジニアに馴染みのある
expressやTypescriptで開発できるServerless Frameworkを使用して

API GatewayAWS Lambda、Amazon DynamoDBの構成で作る
RESTfullなAPIをサクッと作成していきます。

今回利用するツールの紹介

まず今回利用する各サービスを紹介します。

API Gateway とは

APIの作成と管理が簡単にできるサービスです。
どのようなスケールであっても、開発者は簡単に API の配布、保守、監視、保護が行えます。
AWS Lambdaで実行される処理の玄関として振る舞うAPIを作成できます。

docs.aws.amazon.com

AWS Lambda とは

何かしらのイベントによって処理を実行する環境です。
イベントとはAWS上のS3にファイルをアップロードや特定のエンドポイントにアクセス
といった何かしらのアクションのようなものです。
このアクションをトリガーに処理を実行することができます。

docs.aws.amazon.com

Amazon DynamoDB とは

フルマネージドなNoSQLデータベースです。
フルマネージドとは運用をAWSにおまかせでき
利用者はOSやMiddlewareのことを意識する必要がありません。
また、高い拡張性、データへの高速アクセスが可能で
AWS Lambdaとの連携も簡単に行うことができます。

docs.aws.amazon.com

Serverless Frameworkとは

AWS LambdaとAWS API Gatewayを利用したサーバレスなアプリケーションを構築するためのツールです。
作成、管理、デプロイ管理などを簡単に行うことができます。

Serverless Framework Documentation

準備

環境

  • Mac OSX 10.13.4
  • yarn 1.7.0
  • NodeJs 8.1.0

AWSでIAMの設定

Serverless Framework用のユーザーを作成します。

AWSアカウントを作成しIAMユーザー作成ページへ移動します。
https://console.aws.amazon.com/iam/home?region=ap-northeast-1#/users

ユーザーの追加をクリックします。 f:id:yuki-hayakawa-kcf:20180901150019j:plain

ユーザー名を入力し、プログラムによるアクセスにチェックを入れます。 そして「次のステップ:アクセス権限」をクリックします。 f:id:yuki-hayakawa-kcf:20180901150103j:plain

ポリシーのフィルタへ「AdministratorAccess」と入力し「AdministratorAccessへチェックを入れます。 そして「次のステップ:確認」をクリックします。 f:id:yuki-hayakawa-kcf:20180901150155j:plain

入力内容を確認し「ユーザーの作成」をクリックします。 f:id:yuki-hayakawa-kcf:20180901150213j:plain

作成された「アクセスキーID」と「シークレットアクセスキー」をメモします。 f:id:yuki-hayakawa-kcf:20180923170335j:plain

AWS CLI のインストール

Homebrewからawscliをインストールします。

$ brew install awscli
$ aws --version
aws-cli/1.15.50 Python/3.7.0 Darwin/17.6.0 botocore/1.10.49

AWS CLIAWSのアカウント情報を紐付ける

awscliをインストールすると利用できるawsコマンドからaws configureを実行します。
そして先ほどIAMで作成したユーザの設定を紐付けます。

$ aws configure
AWS Access Key ID [None]: AKIAIUKT4JXXXXXXXXXX
AWS Secret Access Key [None]: XXXXXXXXXX
Default region name [None]: ap-northeast-1
Default output format [None]: json

package.jsonを作成

packageはyarn で管理します。

$ yarn init -y

Serverless Frameworkのインストール

Serverless Frameworkはyarnでインストールします。

$ yarn add -D serverless

必要パッケージのインストール

インストールするアプリケーション用のパッケージ

No. パッケージ名 概要
1 @types/aws-lambda aws-lambdaの型定義パッケージ
2 @types/aws-sdk aws-sdkの型定義パッケージ
3 @types/core-js core-jsの型定義パッケージ
4 @types/express expressの型定義パッケージ
5 @types/node nodeの型定義パッケージ
6 @types/webpack webpackの型定義パッケージ
7 aws-lambda AWS Lambdaにデプロイするためのパッケージ
8 aws-serverless-express serverlessでexpressを使用するためのパッケージ
9 path パスを操作するためのパッケージ
10 serverless serverlessを使用するためのパッケージ
11 serverless-dynamodb-local DynamoDB Localを操作できるようにするためのパッケージ
12 serverless-offline ローカルでAPI Gatewayの代用として利用するためのパッケージ
13 serverless-webpack Serverless Frameworkでwebpackのビルドを利用するためのパッケージ
14 ts-loader webpackでtypescriptをトランスパイルするためのパッケージ
15 typescript typescriptを利用するためのパッケージ
16 webpack webpackを利用するためのパッケージ
$ yarn add -D  @types/aws-lambda @types/aws-sdk @types/express @types/node @types/webpack aws-lambda aws-serverless-express path serverless serverless-dynamodb-local serverless-offline serverless-webpack ts-loader typescript webpack webpack-node-externals
No. パッケージ名 概要
1 aws-sdk JavascriptからAWS各サービスを操作するパッケージ
2 body-parser HTTPリクエストボディのデータを取得するためのパッケージ
3 express Node.jsで手軽にサーバを起動したりするためのパッケージ
4 serverless-http Serverless FrameworkでExpressを利用するためのパッケージ
$ yarn add aws-sdk body-parser express serverless-http

ローカルでアプリケーションの起動

Serverless Frameworkの設定

serverless.yml

serverless.ymlの設定ファイルです。
ローカルで動かす設定やdynamodb、Lambdaの設定をここに記述します。

service: demo-serverless
# プラグインの設定
plugins:
  - serverless-webpack
  - serverless-offline
  - serverless-dynamodb-local
# AWS側の設定
provider:
  name: aws
  runtime: nodejs8.10
  stage: dev
  region: ap-northeast-1
  environment:
    DYNAMODB_TABLE: items
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"

package:
  excludeDevDependencies: true
  exclude:
    - serverless-http

custom:
  # webpackの設定
  webpackIncludeModules: true
  webpack:
    webpackConfig: 'webpack.config.js'
    packager: 'yarn'
    packagerOptions: {}
  # Dyamodbをローカルで起動させるための設定
  dynamodb:
    start:
      port: 3030
      inMemory: true
      migrate: true
      seed: true
    seed:
      development:
        sources:
          - table: items
            sources: [./dynamo/items.json]

resources:
  Resources:
   # サンプルで作成するDynamodbのテーブル
    ArticlesTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: items
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

# lambdaの設定
functions:
  app:
    handler: app.handler
    events:
      - http:
          method: ANY
          path: '/'
          cors: true
      - http:
          method: ANY
          path: '{proxy+}'
          cors: true

webpackの設定

webpack.config.js

webpackの設定を記述します。
Typescriptをトランスパイルするts-loader の設定を記述します。

const path = require('path');
const slsw = require('serverless-webpack');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  entry: slsw.lib.entries,
  target: 'node',
  mode: slsw.lib.webpack.isLocal ? 'development': 'production',
  externals: [nodeExternals()],
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        include: __dirname,
        use: {
          loader: 'ts-loader',
          options: {
            transpileOnly: true
          }
        },
      },
    ]
  },
  resolve: {
    extensions: ['.ts']
  },
  output: {
    libraryTarget: 'commonjs',
    path: path.join(__dirname, '.webpack'),
    filename: '[name].js'
  },
};

Typescriptの設定

tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "strictNullChecks": true,
    "newLine": "LF",
    "noEmitOnError": false,
    "sourceMap": true,
    "strict": true,
    "allowJs": true,
    "lib": [
      "es2017"
    ],
    "baseUrl": ".",
    "typeRoots": [
      "./node_modules/@types",
      "@types"
    ]
  },
  "exclude": [
    "node_modules"
  ]
}

Lambdaで呼び出すコードを作成

app.ts

import * as express from 'express';
import * as serverless from 'serverless-http';
import * as aws from 'aws-sdk';
import * as bodyParser from 'body-parser';

const app: express.Application = express();

/**
 * dynamodbClient 開発
 */
const localDynamodb: aws.DynamoDB.DocumentClient = new aws.DynamoDB.DocumentClient({
  region: 'ap-northeast-1',
  endpoint: "http://localhost:3030"
});

/**
 * dynamodbClient 本番
 */
const dynamodb: aws.DynamoDB.DocumentClient = new aws.DynamoDB.DocumentClient({
  region: 'ap-northeast-1',
});

/**
 *
 * IPによって環境ごとのDynamoClientを返す
 * @param {string} ip
 * @returns
 */
const getDynamodbClient = (ip: string): aws.DynamoDB.DocumentClient => {
  return ip === "127.0.0.1" ? localDynamodb : dynamodb;
}

/**
 * bodyParserの設定
 */
app.use(bodyParser.json({ strict: false }));

/**
 * アクセスコントロールの設定
 */
app.use((req: express.Request, res: express.Response, next: express.NextFunction): void => {
  res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
  next()
});

/**
 * アイテム一覧の取得
 */
app.get('/api/items', async (req: express.Request, res: express.Response): Promise<void> => {

  const dynamodb = getDynamodbClient(req.ip);
  const params = {
    TableName: 'items',
    Limit: 100
  }
  const result: aws.DynamoDB.ScanOutput = await dynamodb.scan(params).promise();
  res.json({ items: result.Items });
});

/**
 * アイテムの取得
 */
app.get('/api/items/:id', async (req: express.Request, res: express.Response): Promise<void> => {

  const dynamodb = getDynamodbClient(req.ip);
  const params = {
    TableName: 'items',
    Key: {
      id: req.params.id,
    }
  }
  const result: aws.DynamoDB.GetItemOutput = await dynamodb.get(params).promise();
  res.json({ article: result.Item });
});

/**
 * アイテムの更新
 */
app.put('/api/items/:id/update', async (req: express.Request, res: express.Response): Promise<void> => {

  const dynamodb = getDynamodbClient(req.ip);
  const params = {
    TableName: 'items',
    Key: {
      id: req.params.id
    },
    UpdateExpression: "set title = :title, description = :description, modified_at = :modified_at",
    ExpressionAttributeValues:{
      ':title': req.body.title,
      ':description': req.body.description,
      ':modified_at': req.body.modified_at
    },
    ReturnValues: "UPDATED_NEW"
  }
  try {
    const result = await dynamodb.update(params).promise();
    res.json(result);
  } catch (error) {
    res.json({error});
  }
});

/**
 * アイテムの作成
 */
app.post('/api/items/create', async (req: express.Request, res: express.Response): Promise<void> => {

  const dynamodb = getDynamodbClient(req.ip);
  const params = {
    TableName: 'items',
    Item: {
      id: req.body.id,
      title: req.body.title,
      description: req.body.description,
      created_at: new Date().getTime(),
      modified_at: new Date().getTime()
    }
  }
  try {
    const result = await dynamodb.put(params).promise();
    res.json(result);
  } catch (error) {
    res.json({error});
  }
});


/**
 * アイテムの削除
 */
app.delete('/api/items/:id/delete', async (req: express.Request, res: express.Response): Promise<void> => {

  const dynamodb = getDynamodbClient(req.ip);
  const params = {
    TableName: 'items',
    Key: {
      id: req.params.id
    }
  }
  try {
    const result = await dynamodb.delete(params).promise();
    res.json(result);
  } catch (error) {
    res.json({error});
  }
});

export const handler = serverless(app);

@types/serverless-http.d.ts

公開されている @types がないため作成します。

declare module 'serverless-http';

サンプルデータを作成

ローカルで確認するためのサンプルデータを作成します。

dynamo/items.json

[
  {
    "id": "1",
    "title": "タイトルのテスト1",
    "description": "ディスクリプションのテスト1",
    "created_at": 1532274721534,
    "modified_at": 1532274721534
  },
  {
    "id": "2",
    "title": "タイトルのテスト2",
    "description": "ディスクリプションのテスト2",
    "created_at": 1532274843309,
    "modified_at": 1532274843309
  }
]

DynamoDB Localをインストール

$ yarn run sls dynamodb install
Installation complete!
✨  Done in 48.31s.

DynamoDB Localを起動

$ yarn run sls dynamodb start

ローカルでアプリケーションを起動

$ yarn run sls offline start

ブラウザからアクセス

http://localhost:3000/api/items/

dynamo/items.json に登録したデータが確認できます。

f:id:yuki-hayakawa-kcf:20180901150954j:plain

記事を登録してみる

$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"description":"ディスクリプションのテスト3","created_at":1532274721534,"id":"3","title":"タイトルのテスト3","modified_at":1532274721534}'  http://localhost:3000/api/items/create

ブラウザで確認

http://localhost:3000/api/items/

記事が登録されていることを確認できました。

f:id:yuki-hayakawa-kcf:20180901151025j:plain

記事を削除する

$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X DELETE -d '{}' http://localhost:3000/api/items/2/delete

idが2の記事が削除されたことが確認できました。

f:id:yuki-hayakawa-kcf:20180901151051j:plain

AWSへdeploy

作成したアプリケーションを以下のコマンドでAWSへデプロイできます。

$ yarn run sls deploy -v

AWS側で自動でドメインが割り当てられます。

ServiceEndpoint: https://mi76zpf26a.execute-api.ap-northeast-1.amazonaws.com/dev

ブラウザで確認

ServiceEndpoint で生成されたurlから/dev/api/items/ へアクセスしてみてください。
https://mi76zpf26a.execute-api.ap-northeast-1.amazonaws.com/dev/api/items/

f:id:yuki-hayakawa-kcf:20180901151114j:plain

記事を登録してみる

$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"description":"ディスクリプションのテスト1","created_at":1532274721534,"id":"1","title":"タイトルのテスト1","modified_at":1532274721534}'  https://mi76zpf26a.execute-api.ap-northeast-1.amazonaws.com/dev/api/items/create

実際に記事が登録されていることを確認できます。 f:id:yuki-hayakawa-kcf:20180901151136j:plain

実際にAWSで確認するとAPIが作成されていることを確認できます。

https://ap-northeast-1.console.aws.amazon.com/apigateway/home?region=ap-northeast-1#/apis

f:id:yuki-hayakawa-kcf:20180901151218j:plain

所感

  • 馴染みのあるexpress typescript を使えるため開発がやりやすい
  • 難しいことを考えずコマンド一つでデプロイができるので手軽に使える
  • APIを作りたいときにサクッと作れるため今後の開発が加速できそう

Web制作会社からWeb事業会社(自社サービス)へ転職して感じたメリット・デメリット

KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。  本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。

初めまして、KDDIコマースフォワード(以下KCF)でフロントエンドエンジニアをしている小林です。
主な担当業務としては、Wowma!サイト内のカテゴリ・ランキングページの開発を行っています。

Web業界でのキャリアとしてはKCFが2社目で、前職は小さなWeb制作会社でデザイナーを3年、コーダーを2年ほど経験していました。 今回は制作会社から事業会社(自社サービス)へ転職して感じたメリット・デメリットなどをご紹介したいと思います。

「これからWeb業界に入ろうとしているけど制作会社と事業会社のどちらがいいんだろう?」
「制作会社勤務だけど、事業会社ってどうなんだろう?」(その逆も然り)

といった疑問を抱えている方の参考の一つにでもなれば幸いです。

※すべての制作会社、事業会社が今回の内容に当てはまるわけではありません。あくまで個人の見解となりますので予めご承知おきください。

Web制作会社(前職)で行っていたこと

まず、以前の制作会社で行っていたことを思い出せる限り、洗い出してみたいと思います。

雑務系

  • 電話の一次受け
  • 来訪者の応対
  • オフィスの清掃

コーディング

  • 技術検証(要望の実装が実現できるかの検証)
  • PSDのスライス
  • HTML(EJS)
  • CSS(SCSS)
  • JavaScriptjQuery
  • PHP(素)
  • WordPress
  • デザイナーと都度コミュニケーション(アニメーションのイメージすり合わせ等)

その他

  • 社内会議
  • クライアントとの折衝(ディレクション。ディレクターいなかった)
    • 打ち合わせ(往訪、来訪)
    • 電話
    • メール
    • 打ち上げ
  • マニュアル作成(CMSの操作方法など、お客様用)
  • 納品ファイル送付
  • 技術共有会
  • 会社ブログ執筆
  • 新人教育
  • たまに飲み会
  • たまに徹夜

事業会社(KCF)で行っていること

次に、現在行っていることを挙げます。

コーディング

  • 技術検証
  • HTML(thymeleaf、独自タグ etc)
  • CSS(SCSS, CSS Module)
  • JavaScript(ES2015+, React.js, TypeScript)
  • PO(プロジェクトオーナー)、ディレクターと都度コミュニケーション

その他

  • スクラム
  • 社内会議
  • 勉強会
  • 会社ブログ執筆
  • チームランチ
  • 飲み会
  • 部活、社内イベント

といったところでしょうか。
上記を踏まえつつ、実際に私が感じたそれぞれのメリット・デメリットを挙げたいと思います。

web制作会社のメリット・デメリット

メリットに関しては見出しに☆、デメリットに関しては×をつけています

☆ 様々な種類の案件に携わることができる

  • SNSを使ったキャンペーンサイトを作ってほしい
  • 動画をメインに使ったプロモーションサイトを作ってほしい
  • とにかくインパクト重視でゴリゴリのアニメーションをつけてリッチなページにしてほしい
  • 運用は自社で行うので、WordPressで作成してほしい(マニュアル付き)

etc...

受託制作なので、作る内容は様々です。
ペライチのランディングページから、CMSを使ったコーポレートサイトなど様々な規模や種類の案件に携わることができます。
その分、新しいことにチャレンジできるチャンスが多く、web制作の総合力を付けやすいと思います。


☆ ゼロから自分(自分たち)の手で作り上げることができる

私の場合は、前職の制作会社では既存サイトの改修等よりも、ゼロから新規の作成の方が多かったです。
そのため構成〜デザイン〜コーディング、サーバの手配までを一貫して自分たちで行うため、完成した時の喜びはひとしおです。


☆ 作業スピードが上がる

タイトなスケジュールの中で複数の案件を抱えることが少なくなく、否が応でもスピードが求められるので自然と作業スピードが上がります。
ショートカットやスニペットの登録、テンプレートの作成など、自分の引き出しが増えるにつれて楽になっていきます。
ここで得た引き出しは、その後もずっと自分の財産となるのは大きいメリットだと思います。


☆ 社外の人と繋がる機会が多い

MTGや電話でクライアントと何度もやりとりすることが多いので、自然と繋がりが増えました。
とあるイベント告知のサイトを作成したときは無料でそのイベントに招待して頂いたり、映画のサイトを作った時は試写会に呼んで頂けたり、そういった経験ができたのも受託制作ならではだと思いました。


× 納期が短いことが多く、残業や徹夜が発生する可能性が高い

クライアントとの信頼関係があればスケジュールの調整をしてもらえることもありますが、場合によっては無茶振りにも答えなくてはいけない場面があります。
そんなときは覚悟を決めて作り上げる必要があります。
制作会社がハードと言われる所以でもあります。。


× 技術的にレガシーなものに縛られやすい

案件や状況にもよりますが、クライアントの環境に合わせて古いブラウザに対応させなくてはいけない等、新しい技術をすぐに取り入れることができない状況が多いように感じます。
WebアプリケーションではなくWebサイトの制作がメインであれば今でもバリバリにjQueryをメインに使うことが多いのではないかと思います。

事業会社のメリット・デメリット

☆ チーム開発ができる

KCFではプロダクト毎にチームが分かれており、チームにはプロダクトオーナー(PO)、スクラムマスター(SM)、サーバーサイドエンジニア、フロントエンドエンジニアがおり日々コミュニケーションを取りながらスクラム開発を行なっています。
前職は原則1人で案件を担当していたため、みんなで一つのプロダクトを作って成長させていくのはとてもやりがいがあり楽しいです。
困っていることを共有し助け合ったり、コードレビューをしあったりして自分の成長にもつながります。


☆ プロダクト・サービスを成長させる経験ができる

制作会社では0を1にする(ゼロから作る)ことは数多く経験できましたが、その1を100にするような経験ができませんでした。
ほとんどが納品して終わり、もしくは期間限定の公開で終わりというものだったためです。

事業会社では既存のサービスの運用、改善がメインとなるため、アクセス解析などをして様々な施策を通してKPI達成を目指していきます。
ただ作れば良いというわけではないため、ビジネス的な視点が求められますが、それはそれでみんなで議論することが面白いところでもあります。

技術的な面では、パフォーマンス改善や、他の人が見てもわかりやすいコードにするためにリファクタリングをしたり、運用ルールを統一するためにドキュメントを書いたりと、一人で開発をしていた時には経験しなかったことができて大変勉強になります。


☆ ユーザーの反応をダイレクトに見れる

上記の「プロダクト・サービスを成長させる経験ができる」にも繋がりますがありますが、自分たちの作ったプロダクトがどのようにユーザーに使われているのかを直に見れるのは、事業会社ならではのメリットだと思います。
チームみんなで分析し仮説をたて実装し、狙い通りのユーザーの反応が得られたときは格別なものがあるでしょう。


☆ 新しい技術を取り入れやすい

サービスの成長のためには、よりスピーディーに、より効率よく、より高品質なモノ作りが求められます。
そのための手段として新しい技術を採用することに会社も前向きです。
チームの開発メンバーが良しとすれば、とりあえず使ってみようというエンジニアにとっては嬉しい風土があります。


☆ いろんな社内イベントがある

KCFでは日々の業務以外にいろんなイベントが開催されています。

イベントレポートは下記にて更新中です www.wantedly.com

KCFには部活があり、任意で入部できます。私は先日読書部に入部しました。 勉強会も様々な規模で定期的に開催されていて、通常の業務以外にも知見を増やす機会が多いです。


☆ 働きやすい環境が整っている

こちらも会社によると思いますが、一般的には事業会社の方が福利厚生が整っているところが多いようです。
KCFの制度で個人的に嬉しいのはランチ補助です(半額くらいで買える!)。

そのほかにも社内にカフェテリアがあったり、寝っ転がれる芝生エリアがあったり、パーソナルスペースがあったりと気分転換できるところが用意されていて働きやすい環境が整っています。
また全社的に残業を非推奨としているのでライフワークバランスが取りやすいと思います。


× 会議が多い

入社して最初に戸惑ったのは、会議・MTGの多さでした。
ひとつのサービス・プロダクトをみんなで作っているのでどうしてもMTGが多くなるのは理解できますが、1日作業できずにMTGで終了してしまうこともたまにあります。

最初はMTGに慣れず、MTGに参加して話を聞くだけでいっぱいいっぱいでした。
次のMTGではその前のMTGの内容が前提となってくるので、前のものが理解できていないとどんどんと取り残されていってしまうため理解に必死でした。
今でこそ慣れましたが、最初は議事録を書く習慣を身につけておくと良いように感じます。


× 周りの人を動かす必要がある

開発を進めていく中で、どうしてもチーム内だけで解決しない問題が出てきた場合、部署をまたいで担当者を探して聞いたり、他の人の手を借りる必要があります。
そのような時は能動的に動き、周りの人を巻き込むような動きをしなければなりません。

制作会社の時は人が少なく全員顔見知りなので話しかけやすいですが、社員数の多い事業会社だと初対面の方が多いのでそこをうまくクリアする必要があります。
幸いKCFでは初対面でも依頼したことに対して真摯に対応してくれる方ばかりなので助かっています。


× 障害怖い

自分の作業が原因でサービスに障害が発生すると寿命が縮まるほど焦ります。
古くから運用されているサービスであればコードが複雑化しており、自分の書いたコードが思わぬところでエラーを起こす可能性があります。
すでに運用されているコードであれば、他に影響がないかしっかりと確認する必要があります。

まとめ

私の少ない経験から、双方のメリットとデメリットを紹介させていただきました。

絶対にこっちの方が良い!ということは言えませんが、とにかくいろんなものを作ってみたいという方は制作会社が適していて、チーム開発をしたい・サービスを成長させていきたいという方は事業会社が適しているのではないでしょうか。

ただ、長期的にエンジニアとしてのキャリアを考えるのであれば、まずは制作会社で様々な種類の実装を経験した方が良いと思います。
その方が自分の得意分野を見つけやすいし、基本的なところから幅広いスキルを身につけられると思います。
ここで経験したことが、のちのち事業会社へ転職したとき等に役立つことが数多くあるのではないでしょうか。

個人的には、事業会社であるKCFに転職してから、JavaScriptの細かい仕様に対して興味が出たり、サービス・プロダクトを支える様々な技術を間近で見れて大変勉強になり転職してよかったと感じていますが、これも制作会社で基本を学べたおかげだと思います。
これまでの経験と、これからの知見をもとにWowma!を成長させていきたいです。

iOSDC Japan 2018に登壇してきました!

こんにちは。

iOSAndroidの開発を行なっている高橋(@KoH_1011)です。

8月30日~9月2日、早稲田大学にてiOSDC Japan 2018が開催されました。

iOSDC JapanはiOS関連技術をコアのテーマとした技術者のためのカンファレンスです。

今年は3回目の開催になるそうで、3日+前夜祭の3.5日開催と年々規模が大きくなっていて、みなさんのiOS愛を感じます!

f:id:kosuke_1011:20180904003806j:plain

KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。  本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。

LTルーキーズで登壇

LTルーキーズの枠で登壇してきました。

トークの応募をして実際に採択が決まった日は「ほんとに採択されちゃった」という戸惑いと「あの大きなイベントで登壇できる」という興奮とが入り混じったなんとも不思議な感覚でした。

トークは「RemoteConfigを用いたちょっと変わった運用」というものです。

https://speakerdeck.com/koh1011/a-slightly-different-operation-using-remoteconfigspeakerdeck.com

登壇して何がよかったかと言うと、

  • 資料作りの大変さ、伝えることの難しさを理解した。

  • 登壇することで色々な意見をいただき、こうした方がよさそうだなと気づけたこと。

特に2つ目については登壇することでしか、得られない経験だと思うので、登壇してほんとによかったと思います。

iOSDCの感想

iOSDCでは、iOS開発するうえで楽しいところ辛いところの話。サービスをよくしていきたい。

というエンジニアたちの情熱みたいなところが感じられました。

どこの会社のエンジニアたちも色々なことに悩みサービスを推進していっている姿をみて、自分も負けてられないなと強く思うことができました。

iOSDCで得た知見をいち早くWowma!の開発にも取り込んでサービスを推進していきたいと思います!

謝辞

また、この場をお借りしてiOSDCの運営スタッフの方々にも感謝を述べたいと思います。

とても素晴らしいイベントの企画、運営ほんとにありがとうございました!

ほんとにお疲れ様でした!

来年あれば参加します。また登壇します。

Java × Spring Boot のlocal環境でのProxy設定とSSL証明書の無視

KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。  本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。

Java × Spring Boot のこと

はじめまして、エンジニアの坂本です。
私たちのチームでは Java × Spring Boot で開発を行なっています。
Spring Boot いいですね。

WEBアプリケーション開発で今までJavaが敬遠されて来た理由のほとんど (設定周りの複雑さなどから来る敷居のたかさ)
が解消されていると私は感じています。

ところで、普段の開発では
「こういうことがしたい」ということが明確なのに「どうやったらいいんだろう?」という事がよく起こりますよね。

そういう時にはとにかく「検索」することで、インターネットにある同じ疑問や困難にあたって解決して来た様ざまな人たちの記事に、私は助けられています。

そこで今回は、少しは恩返し・貢献したいという思いから、この記事を書いてます、 よろしくお願いします。

Spring Bootのlocal環境でのHTTP/HTTPSのProxy設定

WEBアプリケーション開発では「外部のAPIに接続する」という必要がよくあります。

しかしlocal環境の開発では(本番環境やステージング環境と違って)社内などの決まったProxyを通して接続しなければいけない、 という状況がよくあると思います。

今回はそんな場合のお話です。

まずは結論のプログラム・ソースコードを載せますね。

package jp.samplesample.application.configuration;

import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource;

import javax.annotation.PostConstruct;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import java.io.IOException;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

/**
 * local環境の時だけHTTP/HTTPSでプロキシーを通るようにしてHTTP接続先のSSL証明書チェックの無効化を設定します。
 */
@Configuration
@Profile("local")
@PropertySource("classpath:proxy.properties")
public class HttpProxyConfiguration {

    @Value("${proxy.host}")
    private String host;

    @Value("${proxy.port}")
    private String port;

    @Value("${proxy.user}")
    private String user;

    @Value("${proxy.password}")
    private String password;

    @PostConstruct
    private void setProxyAndDisableSSLValidation() {
        // Proxy for System
        System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
        System.setProperty("jdk.http.auth.proxying.disabledSchemes", "");
        System.setProperty("http.proxyHost", host);
        System.setProperty("http.proxyPort", port);
        System.setProperty("https.proxyHost", host);
        System.setProperty("https.proxyPort", port);
        Authenticator.setDefault(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, password.toCharArray());
            }
        });
        // SSL証明書チェックの無効化
        disableSSLValidation();
    }

    // local環境用のSSLContextとその初期化
    private SSLContext localSSLContext() {
        try {
            final SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] certs, String s) throws CertificateException {
                }
                @Override
                public void checkServerTrusted(X509Certificate[] certs, String s) throws CertificateException {
                }
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }}, null);
            return sslContext;
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            throw new RuntimeException("sslContext init failed.", e);
        }
    }

    // local環境用のHostnameVerifier
    private X509HostnameVerifier localHostnameVerifier() {
        return new X509HostnameVerifier() {
            @Override
            public void verify(String hostname, SSLSocket ssl) throws IOException {
            }
            @Override
            public void verify(String hostname, X509Certificate cert) throws SSLException {
            }
            @Override
            public void verify(String hostname, String[] cns, String[] subjectAlts) throws SSLException {
            }
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };
    }

    // HTTPS接続先のSSL証明書チェックの無効化
    private void disableSSLValidation() {
        HttpsURLConnection.setDefaultSSLSocketFactory(localSSLContext().getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(localHostnameVerifier());
    }

}

このプログラム・ソースコードのクラスの内容ですが、
まずは内から外へのHTTP/HTTPS通信に関してProxy設定をしています。

しかし、それだけではHTTPS通信は接続先の証明書チェックで引っ掛かるので、証明書チェックを無効化しています。

クラスの先頭に付いている
@Configuraion@Profile("local")
アノテーションが重要です。

@Configurationは設定クラスですよ、
@Profile("local")はlocal環境だけの設定ですよ、
という意味ですね。

このConfigurationクラスを入れておくだけで、local環境の実行においては、どこで通信してもProxy設定が効いているはずです。


おっと「proxy.properties」の説明が抜けていました。

proxy.host=(Proxyのホスト名)
proxy.port=(Proxyのポート番号)
proxy.user=(Proxyにおける自分のユーザー名)
proxy.password=(Proxyにおける自分のパスワード)

上記の内容を書いたテキストを「proxy.properties」というファイル名で保存して、
Spring Boot から見えるリソースとして、
プロジェクト・フォルダの中の /src/main/resources の直下などに置いてください。

RestTemplateの場合はProxy設定が効いている

さて、具体的なAPI通信のほうですが、例えば
普通に直接的に「RestTemplate」を使う場合などは、上記のクラスの設定が入っているだけで大丈夫です。

RestTemplate restTemplate = new RestTemplate();
XXX xxx = restTemplate.getForObject("https://samplesample.jp/api/12345", XXX.class);

よくあるこんな感じですね。

RestTemplateBuilder・RestTemplateCustomizerを使う場合

しかし「RestTemplateBuilder・RestTemplateCustomizer」を使う場合には注意が必要です。

qiita.com

この有益なサイトのRestTemplateに関する記事にある
「3rdパーティ製ライブラリとの連携」の項目に書いてあるように
複数のHTTPクライアントのライブラリがクラスパス上にあった場合には

Java標準のHttpURLConnection (デフォルト)

の検出される優先順位が一番最後だからです。

@Override
public void customize(RestTemplate restTemplate) {
    new RestTemplateBuilder()
        .requestFactory(SimpleClientHttpRequestFactory.class)
        .configure(restTemplate);
}

上記のように私たちのチームでは
Java標準の実装を使うSimpleClientHttpRequestFactoryクラスをrequestFactoryに指定しています。

この指定をしない場合には「HTTPS通信の接続先のSSL証明書チェックの無効化」でコケたと思います。

付録:OpenId4JavaのProxy設定

ここから以下は、非常に限られた用途の場合なので、
「必要ない」という方は読み飛ばしてください。

OpenID認証のクライアントのためのJavaライブラリに
OpenId4Java
というものがあります。

github.com

今わざわざこれを選ぶという人がどれだけいるのか分かりませんが、
私たちのチームは今回は様ざまな事情から、
このライブラリを使う必要がありました。

そしてここでもlocal開発環境でのProxy設定の問題にあたったわけです。

色いろなインターネットのサイトなどを検索して断片などを調べながら、
最後にいま落ち着いているプログラム・ソースコードの状態は、
以下のような感じです。

package jp.samplesample.application.configuration;

import java.io.IOException;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.annotation.PostConstruct;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.openid4java.consumer.ConsumerManager;
import org.openid4java.discovery.Discovery;
import org.openid4java.discovery.yadis.YadisResolver;
import org.openid4java.server.RealmVerifierFactory;
import org.openid4java.util.HttpClientFactory;
import org.openid4java.util.HttpFetcherFactory;
import org.openid4java.util.ProxyProperties;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource;

/**
 * local環境の時だけHTTP/HTTPSでプロキシーを通るようにしてHTTP接続先のSSL証明書チェックの無効化を設定します。<br>
 * あわせてlocal環境用のOpenId4Java向けのプロキシー設定とConsumerManagerの設定もここで行います。
 */
@Configuration
@Profile("local")
@PropertySource("classpath:proxy.properties")
public class HttpProxyConfiguration {

    @Value("${proxy.host}")
    private String host;

    @Value("${proxy.port}")
    private String port;

    @Value("${proxy.user}")
    private String user;

    @Value("${proxy.password}")
    private String password;

    @PostConstruct
    private void setProxyAndDisableSSLValidation() {
        // Proxy for System
        System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
        System.setProperty("jdk.http.auth.proxying.disabledSchemes", "");
        System.setProperty("http.proxyHost", host);
        System.setProperty("http.proxyPort", port);
        System.setProperty("https.proxyHost", host);
        System.setProperty("https.proxyPort", port);
        Authenticator.setDefault(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, password.toCharArray());
            }
        });
        // Proxy for OpenId4Java
        ProxyProperties proxyProperties = new ProxyProperties();
        proxyProperties.setProxyHostName(host);
        proxyProperties.setProxyPort(Integer.parseInt(port));
        proxyProperties.setUserName(user);
        proxyProperties.setPassword(password);
        HttpClientFactory.setProxyProperties(proxyProperties);
        // SSL証明書チェックの無効化
        disableSSLValidation();
    }

    // local環境用のSSLContextとその初期化
    private SSLContext localSSLContext() {
        try {
            final SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] certs, String s) throws CertificateException {
                }
                @Override
                public void checkServerTrusted(X509Certificate[] certs, String s) throws CertificateException {
                }
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }}, null);
            return sslContext;
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            throw new RuntimeException("sslContext init failed.", e);
        }
    }

    // local環境用のHostnameVerifier
    private X509HostnameVerifier localHostnameVerifier() {
        return new X509HostnameVerifier() {
            @Override
            public void verify(String hostname, SSLSocket ssl) throws IOException {
            }
            @Override
            public void verify(String hostname, X509Certificate cert) throws SSLException {
            }
            @Override
            public void verify(String hostname, String[] cns, String[] subjectAlts) throws SSLException {
            }
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };
    }

    // HTTPS接続先のSSL証明書チェックの無効化
    private void disableSSLValidation() {
        HttpsURLConnection.setDefaultSSLSocketFactory(localSSLContext().getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(localHostnameVerifier());
    }

    // OpenId4Java
    // local環境のみ
    @Bean
    @Qualifier("consumerManager")
    public ConsumerManager localConsumerManager() {
        Discovery discovery = new Discovery();
        discovery.setYadisResolver(
                    new YadisResolver(new HttpFetcherFactory(localSSLContext(), localHostnameVerifier())));
        return new ConsumerManager(
                    new RealmVerifierFactory(
                    new YadisResolver(new HttpFetcherFactory(localSSLContext(), localHostnameVerifier()))),
                    discovery,
                    new HttpFetcherFactory(localSSLContext(), localHostnameVerifier()));
    }

}


もちろんlocal環境以外の場合には、
OpenId4JavaのそのままのConsumerManagerを使わないといけないので、
以下の設定クラスを入れています。

package jp.samplesample.application.configuration;

import org.openid4java.consumer.ConsumerManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
public class OpenId4JavaConfiguration {

    // local環境以外
    @Bean
    @Profile("!local")
    @Qualifier("consumerManager")
    public ConsumerManager consumerManager() {
        return new ConsumerManager();
    }

    // local環境用のConsumerManagerの設定はHttpProxyConfigurationクラスに宣言してあります

}

【真面目系PO(PeyoungOwner)が語る・第二弾】今更ながら新社会人に送るビジネスカタカナ用語の意味5選!

KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。  本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。

はじめに

ALOHA〜!
はじめましての方もそうでない方も、渋谷のGIGA MAXことYontaroです。

▼Yontaroって誰だよという方はこちら kcf-developers.hatenablog.jp

あらすじ

前回の記事に対して全く中身ないやん!と指摘を受けたyontarou
ショックをうけている彼の前に新たな難敵が舞い降りた!!
会社中、至る所で交わされる意味不明な横文字の羅列だ。
彼はそれぞれの意味を理解し、全て打ち倒すことができるのか…

というわけで、4月から現場に入った新入社員の方や、研修を終えて7月から現場に入った方、もしくはもう10年目なのに漢文しか理解する気のない漢気溢れる方達に送る、IT現場でよく交わされる言葉の意味解説になります。

よくある例と間違った例を両方のせることにより、理解を深めることを目的としています。

とにかくよくわからない言葉たち

アグリー


これは頻繁に聞く機会があるのではないかと思います。英語がわかる方ならすぐにピーンとくるでしょうね。
まずは、実際の使用例をみていきましょう。

よくある例

新人のシンジ『私は今回、こういう考えで進めていこうと思います』
部長『あー!うんうん!私もそれアグリー!!』

f:id:yontarou:20180731114529p:plain
※何故か怒っている人

ということですね。

つまり簡単に言うと同意した時に使っておけば大丈夫です。英語のagreeからきているんですね。

間違った例

なんらかの伝承者『貴様はもう死んでいる』
部長『あー!うんうん!私もそれアグリー!!アグアグアグアグリィィォ!!!』

死んでます。

アサイ


勘弁してくれ。そう思った方も多いでしょう。また数学か、サインコサインタンジェントか。違います。これも使用例をみたら簡単にわかります。

よくある例

部長『あの炎上しているプロジェクトがあるじゃろ。今度君をあそこにアサインすることにした』
新人のシンジ『』

わかりましたね。

危険なプロジェクトに任命される時によく使われる言葉です。
配属とか任命とか、そういったわかりやすい言葉を使わずに、アサインといった西洋かぶれになる事で、煙に巻こうとしているんですね。こちらも英語のassignからきています。

間違った例

スネオ先輩『君、ここのコードどうなってるの?ねえ?いい加減にしてくれないかな???それにこのエディタ一人用だから君のぶんはないよ』
新人のシンジ『っせえぞ!!ネチネチしやがって!てめえの頭を目の前のモニタにアサインしてやろうか!?』

?!
マガジンじゃないんだから。

幸いにもKDDIコマースフォワードはお互いを尊重し合っているので、こんな会話は生まれません。

ファンダメンタル


いきなり難易度があがりましたね。
メンタルはなんとなくわかる方も多いでしょうが、ファンダとは…?パンダと言いたくて噛んでしまったのか?パンダメンタルなのか?

そんなわけがない。

とにかくここも例をみてみましょう。

よくある例

部長『とにかくファンダメンタルが重要なんだよ!まず、ファンダメンタルをおさえていかないとダメだよ』
とにかくメンタルは強い原田『お言葉ですが…僕はメンタル強いですよ。パンダは超えてます』

バカか

パンダは超えてますって、パンダのメンタルどんだけやねん。
これはちょっと勘違いしてますね。
ファンダメンタルは基本的なとか基礎的なとかそういった意味で使われます。
つまりこのケースですと、基本が重要だから、まず基本をおさえよう!と部長は仰られているのですね。

間違った例

とにかくメンタルは強い原田『お前らも俺のファンダメンタル見習ってさ、とにかくメンタル強くなろうぜ!世の中ファンダメンタルだよファンダメンタル!』

確かにここまで言い切れるのはメンタル強い。

リスケ


異常な頻出頻度を誇るこのワード。
IT業界に身をおいたことがあれば一度は聞いたことがあるでしょう。
馴染み深い、でもわからない。しかし使用例をみたら簡単に理解できてしまいます。

よくある例

新人のシンジ『申し訳ありません。こちらのリリース日をリスケさせていただけないでしょうか!』
部長『おっけー!』

お見事!真摯な態度で接することによって、中年男性の肩より凝り固まった部長の牙城を崩しましたね。
そうなんです、リスケとはリスケジュール(reschedule)の略で「計画を変更する」とか「スケジュールを組み直す」時に使われます。

間違った例

新人のシンジ『部長!この食べ残しのリスケ貰っちゃっていいですか!』
部長『おっけー!』

ビスケットじゃねーよ。

サマリー


長かったこの記事もいよいよ最後となりました。
これも非常に出てきやすい単語ですね。
サマリーだかサラリーだかわかりませんが、特に打ち合わせ時に出てきやすいものとなっています。

よくある例

会議の達人カワモト『ねえ、このミーティングの目次は?あと前回のサマリーないの?サマリー』
新人のシンジ『さ、サマリーですか...?』

というわけなのです。

ここからわかるように、サマリーとは「概要」とか「要約」「まとめ」といった意味なんですね。前回のまとめないの?そんな気軽な気持ちでカワモト氏は聞いていたのでしょう。これからサマリーという言葉が出てきても焦らずに対応することができますね。
そうです、答えは「サマリーはありません」です。

間違った例

会議の凡人ヤマゼン『君さ、そういうところがサマリーなんだよね。サマリー!わかる?』
新人のシンジ『わかるかい!』

完全に意味を理解せずにファッション感覚で使ってますね。

まとめ

はい、というわけで今回は新たにビジネス用語を5つも覚えてしまいました。
これらを使いこなせば、まわりからは一目置かれる存在になること請け合いです。

最後に今日の復習として、5つ全ての言葉を取り入れてよくある例を提示して終わります。

よくある例

新人のシンジ『今回の会議のサマリーはこちらとなっており、重点的にお話したい部分はスケジュールのリスケについてです』 とにかくメンタルは強い原田『すげーじゃん!まじファンダメンタルだよ君!!』 部長(なんだこいつ頭おかしいのか...) 新人のシンジ『ありがとうございます。リスケの理由としてはこれこれで、申し訳ありませんが承認をいただけないでしょうか』 スネオ先輩『その理由じゃ仕方ないね、アグリーです』
部長(いや理由って「これこれ」でなんでわかるんだこいつ...)
部長『それって人を追加でアサインすることで解決したりはしないの?』
新人のシンジ『あ、アグリーじゃないんですか...?サマリーもリスケも使って説明したのに!?アアアア!!』
部長『どうしたんだ!落ち着きたまえ』
とにかくメンタルは強い原田(全然こいつファンダメンタルじゃなかったわ。やはり俺こそがファンダメンタル)
新人のシンジ『アガアガアアガアガアアアアアア!!!アアアアアアアアアアァァァァァァァ!!!!!』

はい、無理にカタカナ用語を頻発するとメンタルに影響することが多いですね。
カタカナ用語は用法容量を守って正しく使用しましょう。
言葉なんてね、意味が伝わって相互に理解できるのが一番なんです。
無理にカタカナ用語で喋らなくても、自然体で喋るのが一番ということですね。

※今回の登場人物は全て架空であり、実在致しません。


webpack-dev-server後継のwebpack-serveを利用してみたお話

2018.09.20 追記: webpack-serveは DEPRECATED となりました。 これまで通り webpack-dev-server を利用することをおすすめします。

はじめまして。エンジニアの白本です。

皆さんはフロント開発における開発用ローカルサーバは何を利用されていますか?
browser-syncwebpack-dev-server
私は少し前からwebpack-dev-serverの後継であるwebpack-serveを利用しています。

github.com

KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。  本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。

webpack-serveってなに?

f:id:kcf_shiramoto:20180806163133p:plain
webpack-dev-serverのgithubには以下のように書かれています。

Please note that webpack-dev-server is presently in a maintenance-only mode and will not be accepting any additional features in the near term. Most new feature requests can be accomplished with Express middleware; please look into using the before and after hooks in the documentation.

Use webpack-serve for a fast alternative. Use webpack-dev-server if you need to test on old browsers.

要はwebpack-dev-serverはメンテナンスモードで運営されていて今後新しい機能などは追加しないからwebpack-serveを使ってね!
ただし古いブラウザ使うんだったらwebpack-dev-serverが必要だよ。

webpack-serveとwebpack-dev-serverの違い

webpack-serveとwebpack-dev-serverの違いを見てみましょう。

利用できるwebpackのバージョン

webpack-serveはwebpack v4以降でないと利用できません。*1
webpack-dev-serverも最新のv3.xではwebpack v4.xでしか利用できませんがバージョンを落とせばwebpack v3.xでも利用できます。

内部で利用されているフレームワーク

webpack-dev-serverは Express を利用しているのに対しwebpack-serveは Koa を利用しています。

KoaとExpress

本題から外れて少しだけKoaとExpressの話をしましょう。
どちらもnode.js用のWebフレームワークです。またKoaとExpressの作者は同じ方です。
Expressが2010年にリリースされ、後発のKoaは2013年にリリースされています。
Koa自体はExpressが持つルーティングやテンプレートの機能をそれ単体では持たない軽量なフレームワークです。
それらの機能が必要であればパッケージを追加する必要があります。
また大きな違いとしてKoaはジェネレータやasync/awaitという比較的新しい機能を利用でき、Expressで問題だったコールバック地獄から解消されます。
より詳しく違いを知りたい方は公式の Koa vs Express を読んでみてください。

WebSocketかSockJSか

LiveReloadやHMRの仕組みが違います。
webpack-serveは WebSocket (webpack-hot-client経由で)というモダンなウェブブラウザでは標準で利用できる通信規格を利用しています。
webpack-dev-serverは SockJS というライブラリを利用しています。
SockJSはWebSocketが利用できるブラウザ・環境ではWebSocketを利用しますが古いブラウザやその他の制限で利用できない場合はポーリングなどの代替手段を提供します。
古いブラウザではwebpack-dev-serverを利用する必要があるのはこのためです。

実際に使ってみる

Config

webpack-dev-serverはwebpackコンフィグにオブジェクトで設定しますがwebpack-serveにはいくつかの方法が用意されています。

1.CLIで指定

詳しくはgithubを見てもらえれば分かりますがCLIで指定する方法があります。
簡単な設定であればこちらで問題ないですが長くなる場合は別の方法がいいでしょう。

$ webpack-serve --port 3000 --reload --config ./webpack.config.js

2.package.jsonに書く

package.jsonJSON形式で書くこともできます。

...,
"serve": {
  "port": 3000,
  "reload": true,
  "config": "webpack.config.js",
  "content": "./public",
  "devMiddleware": {
    "publicPath": "/assets"
  }
}
...

3.JSONYAMLファイルに書く

2で書いた内容をJSONYAMLファイルとして書くこともできます。
package.jsonはパッケージの情報やnpm scripts等も記載されますので分けて書きたい場合はこちらの方がスッキリするでしょう。
.severc.serverc.json,.serverc.yml というファイル名で保存します。

4.webpack.config.jsファイルに書く

webpack.config.jsにも書くことができます。

// webpack config
module.exports = {
 ...
};

// serve config
module.exports.serve = {
  port: 3000,
  content: './public/,
  devMiddleWare: {
    publicPath: '/assets/',
    stats: {
      cached: true,
      modules: false,
      colors: true
    }
  }
};

5.serve.config.jsに書く

こちらもJSONYAMLと同じで4で書いた内容をwebpack.config.jsと切り離して設定ファイルを用意したい場合に利用します。
serve.config.js というファイル名で保存します。

Events

イベントをハンドリングしたい場合はJSを利用するしかありません。
全てのイベントの第一引数からStatsを参照できるので追加で情報を出したい場合はこちらを利用すると良いでしょう。

module.exports.serve = {
  ...,
  on: {
    'build-started': (stats) => {
      console.log('ビルドを開始しますよ');
    },
    'build-finished': (stats) => {
      console.log('ビルドが終わったよ');
    },
    'compiler-warning': (stats) => {
      console.log('Warningが発生したよ');
    },
    'compiler-error': (stats) => {
      console.log('Errorが発生したよ');
      /**
       * エラーは通常設定であれば敢えてこちらで出力する必要はありません
       * 敢えてこちらで出力したい場合は stats.errors を false にしないと2重でエラーが出てしまいます
       */
      const { errors } = stats.json;
      errors.forEach((error) => {
        console.log(error);
      });
    }
  }
};

Add-on

webpack-serveの目玉?機能のひとつにAdd-onがあります。
webpack-serve自体に機能を持たせるのではなく、必要があれば自由に追加することができます。
この辺はKoaに似ていますしKoaをフレームワークとして採用した理由のような気がします。
githubにいくつかのサンプルが用意されています。
また、Add-onを利用したいくつかのパッケージも既にあるようです。

github.com github.com

おわりに

webpack-serveは今年2月に最初のバージョンがリリースされました。
5月に1.0.0がリリースされ、7月に2.0.0がリリースされています。
1.0.0から2.0.0へのアップデートでは破壊的変更があり設定の互換性がありません。
短いスパンでのメジャーアップデート、破壊的変更からも分かるようにまだまだ試行錯誤段階のようです。

これらのことから今すぐwebpack-dev-serverからwebpack-serveに乗り換える必要はないでしょう。
ただし最初に書いたようにwebpack-dev-serverは既にメンテナンスモードです。
今後webpackがメジャーアップデートされるタイミングでwebpack-dev-serverは対応されない可能性もあるので今のうちからwebpack-serveに慣れておくといいですね。

*1:執筆時のバージョン2.0.2

【真面目系PO(PeyoungOwner)が語る・第一弾】アプリチーム結成から半年経った現在について振り返る

KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。  本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。

はじめに

ALOHA〜〜!
ハワイにいったことはないが、心は常に常夏!アプリチーム唯一の良心!
そう、それが私Yontaroです。

f:id:yontarou:20180712194158p:plain
※ハワイ太郎として四人目の太郎を狙っています



突然ですが...!
今、私は猛烈にお腹が張っています
何故ならお昼にペヤ◯グGIGA MAXを食べてしまったからです。

wowma.jp

知ってます?これに使うお湯の量って1.3Lですよ!?
150杯くらい食べたら、湯船に浸かるのと同じレベルになっちゃいますね!
実際これを食べた直後はヘブン状態*1に陥っていて、思わず歌を口ずさむ始末です。

ペ◯ング それは 君がみた白い箱
ぼくが 食べた カロリー
ペヤ◯グ それは 混ざらないソース
幸せの 茶色いそば ◯ヤング

ジーーーンときちゃいましたね。

さて、ここまでで十分Wowma!アプリチームの雰囲気は感じ取れたかと思います。
というわけで、今回はそんなチームの歴史に関して振り返っていきましょう。

チーム結成

私が入社したのは2017年の10月頃。
前職の有休消化中は職場*2の雰囲気に馴染めるかどうかだけが気になり、入社時の挨拶を1日中考えていました。

もっとも考えた挨拶は時事ネタが多かったので、結果的に直前に考え直すという全く意味のない時間を過ごした時期でもありました。

まあ人生、意味のない時間の方が多いと思っておりますので、そこは問題なかったのですが、問題だったのはむしろ入社後にチームがまだ存在していなかったということでしょうか。

そんな中、部長の「これからアプリを推していくんだから、専用チーム必要だよね!?」*3といった一声でアプリプロダクトとしてのチームが結成されたのでした。

めでたし、めでたし





f:id:yontarou:20180712195434p:plain




カンフー映画かよ!



気を取り直しまして...さて当時、どんなメンバーがいたのか気になるのではないでしょうか。
しかし、初期メンバーは...大変申し訳ありません。

私の口からのべることが大変難しい状況です。

本当に申し訳ありません。
やはり、メンバーにもプライバシーといったものがございまして、許可を得ずに勝手に書くわけにはいきません。

m(_ _)m
...
..
.





イカれたメンバー 紹介するぜ!

まずは、青◯Yontaro

◯雲
yontaro

以上だ!



wowma.jp

苦難のデイリー

そんなこんなでチーム結成にいたった私たちですが、やはり出てくるのは毎日の情報共有問題です。
ここは先人の知恵を拝借しよう、ということでデイリースクラムを画策した我々ですがそこはご存知の通り、決まった時間にくるのが苦手な私ですから、必然的に開催が危ぶまれます。

当初10時から開催されていたデイリースクラムも、開始時間が1時間ずつ遅れていき、最終的に夕方の会議へと進化。更にはその夕会までも消滅。




なんでこうなってしまったのか?
それを把握するべく、脳内で振り返りを行ってみました。




f:id:yontarou:20180713120935p:plain
※振り返りなのにお金のことしか考えていない人


な〜んだ


振り返るまでもない。非常に簡単なことでした。

「決まった時間に人(主にYontaro)がいない」

時間を守って打ち合わせを行うというのは、人生において一番大事なことの一つかもしれません。
と、これだけで終わってしまうと、どうしようもない人が集まっているチームといった印象を与えてしまうので
勿論それだけではないことを伝えておきたいです。

毎日・毎週など決まった時間に実施される会議は基本的に形骸化してしまう。
それは何故なのか?誰もつきとめることのできなかった謎を解明しようとして命を落とした若者が
当時「興味を引かぬものは意味がない」と言いきり、カルト的人気を集めていたテイレ=イ=ミナシだ。
彼は息をひきとる直前に、目的もなく決まった時間に行う会議は意味がないと断言し、安らかな笑顔でこの世を去った。
人々が疑問に感じていた、定例会議時のもやもや感を言語化した彼の偉大な功績を讃えて「内容のない定例会議は意味がない」と現代まで伝えられている。
民明書房刊『世界の打ち合わせ達人伝』より


そうなんです、つまり会議の内容が悪かった。
私たちも人間ですから、面白くないものを毎日ダラダラとやりたくないわけです。
それは打ち合わせも然り。

まあ、今思えばデイリースクラムの内容に問題があったんですね。

だけど当時の私たちはそこに思いを馳せることなく、こりゃアカンとなり、PDCAでいうところのActionで
朝会の終了を選択してしまったのです。

これはいってみれば、しりとりでいきなり「ん」を使って終わらせてしまったようなものでして、
おいおい!それって終わっちゃうじゃん!しりとりって文字を繋げていくんだよ!
こういった基本も怠って、改善活動の「か」の字も行わずに終了へと持ち込んでしまいました。

この時の反省が今になって生きているというのはありますね。

振り返り〜そして消滅?

f:id:yontarou:20180713115110p:plain

さて、デイリーすら続かないチームとなってしまった我々。
巷ではレトロスペクティブと呼ばれている振り返りはどうなのでしょうか?

スクラムとか関係なく振り返りって大事だよね!KPT*4だよね!
そう日頃から信念をもって生活していた我々は毎週振り返りを行っていました。

しかし、折角だしたTryが全く実践できない。
そんな毎週毎週KEEPなんてでねーよ!などといった心の声を聞きつつも、懸命に運営を行っていましたが断念。

f:id:yontarou:20180713190850g:plain
※間違った振り返り

原因の一つは人数が多すぎて時間がかかりすぎたことにあると考えています。
当初数名からはじまっていたアプリチームも、振り返りを断念したタイミングでは10名以上に増加していました。

三人寄れば文殊の知恵とはいいますが、集まりすぎても非効率なんですね。
なので我々は苦渋の決断を下したのです。

そう、振り返りをネイティブアプリを行うチームとアプリAPIを取り扱うサーバチームの二つに分割してしまったのです。
これが功を奏したのか、私が不参加を決めたことがよかったのかわかりませんが、なんとこの振り返りは半年以上たった今も続いています。

いやあ、振り返りって本当にいいものですね。

現在

そんなこんなで波乱万丈なチーム人生を送ってきたアプリチームですが、現在はエンジニアを10名以上抱え、チーム全体では16名ともなる巨大なチームへと成長しました。
最近では新しいスクラムマスターの方も入り、和気藹々としながらも、日々真剣にお客様へ感動を与えるべくアプリの開発を行っております。

前半とはうってかわって後半突然真面目に紹介し始めるとは、書いている本人も思いませんでした。
やはりおかしな人を演出しようとしても、人間本来の性質が出てしまう。
昔の偉い人も言っておりました。

なので現在のチーム状況を語る際に真面目になってしまうのは、いわば必然であります。
そうなってくるとおそらく気になるのは、チーム構成でしょう。

書いていいのかわかりませんが、これが世に出ているという事はOKがでたということでしょう。


イカれたメンバー構成※2018年7月現在

▼エンジニア
ネイティブアプリ:5名
サーバサイド:5名

▼デザイナ
Pythonマニア: 1名

▼PO/SM
PO: 1名
SM: 1名

▼その他
ウィスパーMatsuno: 1名
レビュースペシャリスト: 1名
Yontaro: 1名

といった完璧な布陣!


*1:気になる方はGoogleで検索してみてください

*2:KDDIコマースフォワード

*3:推しメンバーとかチームXXとかそういう話ではありません

*4:Keen Problem Throw=泣き叫びながら課題を投げ捨てる様

ちゃらおのスクラム日記

KCFでプロダクト開発をスクラムで回してる神保です。

f:id:Hjinbo:20180608191104j:plain:w200

今回はスプリントの終了と始まる前に行うレトロスペクティブについて少しお話を・・・。

KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。  本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。

レトロスペクティブって

よくみんながプロジェクトの終わりにやっている振り返りだよね。 次の開発に進む前に一回振り返って、次の開発をより良くするために行うものって考える。 レトロスペクティブってスクラムガイドによると「スプリントレトロスペクティブは、スクラムチームの検査と次のスプリントの改善計画を作成する機会 である。」 https://www.scrumguides.org/docs/scrumguide/v2017/2017-Scrum-Guide-Japanese.pdf

 振り返りって後ろ向いてるけど何のため?➝前に進むためだよね? by スクラムコーチ 

我々の問題点じゃないかと思われるとこ

ここからが本題になるのだが、スプリント回してるとこんなことない?

「発言する人が限られて声の大きい人のTRYを毎回するようになる」 少なからず今まで開発してきたチームでは、そういった人たちがいた経験がある。

今回はその声の大きい人が悪いって話ではなく「全員の意見聞いて、合意してTRYできてるか?」って事を話したい。

少なからず自分も↑の声の大きい人の一人。 普段から気をつけているが、他人から見たときそうなってないかもって客観的に自分を見てみたときハッと怖くなる((;゚Д゚)

スプリントを回せば回すほど形骸化してきて↑の事象に陥りやすいのでは?

チャレンジ

今までは一般的な「keep」「problem」をだして「try」をディスカッションするやり方で 1Hかけてレトロスペクティブのイベントを回してた。

f:id:Hjinbo:20180717151939j:plain

んで、今回は手法を変えてみた。 ルールは以下

①スプリントの気分を表現する

  • スプリント大変だった?

 超大変 ↔ ちっとも

  • スプリント楽しかった?

 サイコーに楽しい ↔ ぜんぜん

  • 次はもっとうまくできそう?

 できる ↔ もう限界

↑の三点を各質問毎にチーム全体で横一列に並んで表現してみる。

f:id:Hjinbo:20180717152025j:plain

②付箋に変えたいことを書く個人で書いて共有する

1>ふせんを1人3枚書く

2>部屋を歩き回り、他の人に共有

3>なぜそうしたいのか説明

4>他の人のふせんと交換する

③改善したいところを決める

1>手元の付箋から一人1枚選ぶ

2>一人一人なぜを説明する

3>正の字投票する

④議論ディスカッション

1>同じものを選んだ2、3名でディスカッション

2>より良いこうしたいを議論する

3>こうしたい案を提出する

⑤チーム合意する

1>提出したものを親指を↑GOOD ↓BADで表現

2>看板ボードへ

やってみて総括

①②では みんなニヤニヤ恥ずかしそうに「俺フロント実装おもったより早くできたから楽だった」、「今回は調整ばっかでつまんなかった」なんて楽しそうにやっていた。

普段コミュニケーション薄いメンバーとの交流があった?かな 。

④では全員でディスカッションするときより、スピードが断然に早く、いつもより多くの改善案が提出された。

加えて、いつも2,3人でのディスカッションが制約になって 発言をしないといけない状況を生んでいるので、より個人が積極的にならないと行けないと感じた。

GOOD↑

・全員がちゃんと意見言える場になった

・普段コミュニケーション取らない人と話せる

・ポジティブにならないと行けない場を生んだ

・楽しい

BAD↓

・時間がかかった(1Hちょい)

・切り替えてやることが多いので人数多いとルールを守る統率が大変

そろそろ疲れたので今回はここで終了。気分でまた書きます

MVVM+Fluxを試してみた

こんにちは。

現在、iOSAndroidの開発を行なっている高橋洸介(@KoH_1011)です。

Wowma!アプリではMVVMのアーキテクチャを使用しています。

ただ、最近Fluxもいいよ!という声をよく聞くので、現在採用しているMVVMとFluxを合わせた実装を試してみたいと思います。

この記事を読むことで以下のことがわかる。というのを目標に書いていきます。

・Fluxについてざっくり理解できる

・Fluxのコードのイメージができる

KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。  本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。

Fluxとは

FluxとはFacebookが提唱したUIを持ったアプリを作るためのアーキテクチャです。

公式では以下のように説明されています。

Flux is a pattern for managing data flow in your application. The most important concept is that data flows in one direction. As we go through this guide we'll talk about the different pieces of a Flux application and show how they form unidirectional cycles that data can flow through. Fluxは、アプリケーションのデータフローを管理するためのパターンです。最も重要な概念は、データが一方向に流れることです。このガイドでは、Fluxアプリケーションのさまざまな部分について説明し、データが流れることができる単方向サイクルをどのように形成するかを示します。

Fluxは以下の図でよく説明されています。

f:id:kosuke_1011:20180608162133p:plain

図でわかるようにデータが単一方向に流れるので非常に見通しがよくなるほか、「データはすべて Action を介して更新する」という制約があるため、 View の更新状態を予測しやすくなります。

それぞれの責務を説明すると、

・Dispatcher:発火された ActionStore に通知するもの

・Store:dispatcher 経由できた Action を格納するもの

・Action:View 等から発火されたイベントを dispatcher に通知するもの

・View(ViewController): Store のデータを反映するもの。イベントを Action に通知するもの

開発環境

Xcode:9.4

・Swift:4.1

仕様

QiitaAPIを使って以下のことを実現させていきます。

ViewController に記事がリスト表示されている。

DetailViewController にお気に入り状態が表示されている。

ViewControllerDetailViewController のお気に入り状態が同期されている。

使用するライブラリ

まずは使用するライブラリを取り込みます。

使用するライブラリは実際にWowma!アプリでも使用している以下のものを使っていきます。

・APIKit

・Himotoki

・RxSwift

Model

Himotoki を使って Model の生成をしていきます。

今回はお気に入り状態の更新を見ていきたいので、 titlefavorite の2つを定義しています。

Request

APIKit を使って Request の生成をしていきます。

リクエスト先は (https://qiita.com/api/v2) になります。

このAPIで新着記事一覧を取得できます。

Flux

Fluxには以下の要素が必要なので、実装していきます。

・Action

・Dispatcher

・Store

Action

まずは Action の実装をしていきます。

今回の仕様は

ViewController に記事がリスト表示されている。

DetailViewController にお気に入り状態が表示されている。

ViewControllerDetailViewController のお気に入り状態が同期されている。

なので、 Action で実装するイベントは以下の2つになります。

① 一覧の取得

② お気に入り状態の更新

まずは

① 一覧の取得

①一覧の取得 では、実際にリクエストを投げてそのレスポンス [Article]dispatch します。 エラーの場合も dispatch する必要がありますが、今回は割愛します。

dispatch の処理は後ほど Dispatcher の箇所で実装していきます。

実際にリクエストを投げる処理を実装していきます。

func load() {   
    let request = ArticleRequest()
    Session.send(request) { (result: Result<ArticleRequest.Response, SessionTaskError>) in
        switch result {
        case .success(let articles):
            // 一覧の取得
        case .failure(let error):
            // エラー
        }
    }
}

上記の処理でレスポンス [Article] を取得できました。

次に取得したレスポンス [Article] を実際に dispatch していきます。

後ほど実装しますが、 dispatcher が必要になるので、初期化メソッドで dispatcher を生成します。

private let dispatcher: ArticleDispatcher

init(dispatcher: ArticleDispatcher = .shared) {
    self.dispatcher = dispatcher
}

あとは後ほど Dispatcher で実装する dispatch メソッドに値を渡すだけです。

self.dispatcher.dispatch(obj: articles)

② お気に入り状態の更新

大枠は上記の実装で終えているので、 dispatch する関数だけ追加します。

お気に入り状態の更新で必要な情報は Article なのでこれを引数にして dispatch します。

こんな感じ。

func update(article: Article) {
    self.dispatcher.dispatch(obj: article)
}

以上で Action の実装は終わりです。

Dispatcher

続いて Dispatcher を実装していきます。

Action から来るイベントは以下の2つになるので、①の関数と②の関数を実装していきます。

① 一覧の取得

② お気に入り状態の更新

先ほど Action の実装で出てきた dispatch をここで実装します。

func dispatch(obj: [Article]) {
    self.articles.onNext(obj)
}

func dispatch(obj: Article) {
    self.article.onNext(obj)
}

dispatch する関数ができたので、これを Store に通知する仕組みを実装していきます。今回は通知する仕組みに PublishSubject を使用したいと思います。こんな感じですね。

let articles = PublishSubject<[Article]>()
let article = PublishSubject<Article>()

また、複数インスタンスだとデータを受け取った受け取ってないということが起きてしまうので、 singleton で実装します。

static let shared = ArticleDispatcher()

Store

続いて Store を実装していきます。

Store で必要な情報は Dispatcherdispatch した PublishSubject を購読して処理に移すことです。

また、購読した値を View に反映させる責務もあるのでその実装もしていきます。

購読するものは Dispatcher で実装した articlesarticle になるので、まずは初期化メソッドにて dispatcher を受け取ります。

required init(dispatcher: ArticleDispatcher = .shared) {

    super.init(dispatcher: dispatcher)
}

dispatcher を受け取ったらそれをもとに購読をしていきます。

/// dispatcherのarticlesの購読
dispatcher.articles.subscribe(onNext: { [weak self] (articles) in
    // 処理
}).disposed(by: disposeBag)

/// dispatcherのarticleの購読
dispatcher.article.subscribe(onNext: { [weak self] (article) in
    // 処理
}).disposed(by: disposeBag)

次に購読したあとの処理を実装していきます。

必要な実装としては以下になります。

・articlesの更新処理

・お気に入り状態を更新する処理

更新処理の実装をする前に View の更新に必要な articlesarticle を定義します。

BehaviorRelay についてはこちらの記事に概要が記載されています。

private(set) var articles = BehaviorRelay<[Article]>(value: [])
private(set) var article = BehaviorRelay<Article>(value: Article())

定義をしたら実際の更新処理を書いていきます。

articles の更新は一覧の更新になるので、値を特に変更せずにそのまま更新します。

self?.articles.accept(articles)

article の更新はお気に入り状態の更新になるので、詳細用の article と 一覧用の articles の両方を更新する必要があります。 また、更新する際にお気に入り状態も更新します。 お気に入り状態を更新する際は ID 等で比較して更新するのがベターですが、今回は比較できるものが title しかないので、 title の一致でお気に入り状態を更新します。

guard var articles = self?.articles.value else { return }
articles.enumerated().forEach { (index, value) in
    if value.title == article.title {
        articles[index].favorite = !articles[index].favorite
        self?.article.accept(articles[index])
    }
}
self?.articles.accept(articles)

以上で Store の実装は終わりです。

TopView

Flux の肝となる部分の実装が終わったので、 Flux を用いながら MVVM の実装をしていきます。

まずはトップの記事一覧で使う ViewModel を実装します。

ViewModel では Action の実行と Store を購読します。

Action の実行は簡単ですね。

そのまま呼ぶだけです。

func load() {
    self.action.load()
}

func update(article: Article) {
    self.action.update(article: article)
}

Store の購読も簡単ですね。

こんな感じです。

self.store.articles
    .asObservable()
    .bind(to: _articles)
    .disposed(by: disposeBag)

bind してる先は BehaviorRelay を定義してあります。

private(set) var articles = BehaviorRelay<[Article]>(value: [])

ここまでの流れをみるとやりたいことは1つですね。

articlesTopViewController で購読させます。

ViewController では以下のように購読させて自前で作成した TopDataSourcebind します。

長くなりましたが、これで記事の一覧を表示することができました。

お気に入りもできます。

viewModel.articles
    .asObservable()
    .bind(to: tableView.rx.items(dataSource: dataSource))
    .disposed(by: disposeBag)

記事一覧

f:id:kosuke_1011:20180621150131g:plain

DetailView

記事の一覧の実装が終わったので、あとは詳細画面の実装をしていきます。

Flux の部分は先ほどの実装で終わってるので詳細用の ViewModel を実装します。

先ほどの ViewModel の実装と同じように Action の実行と Store を購読していきます。

Action の実行は例によって実行するだけになります。

func update(article: Article) {
    self.action.update(article: article)
}

Store の購読も先ほどとほぼ同じです。

self.store.article
    .asObservable()
    .bind(to: article)
    .disposed(by: disposeBag)

ここでも bind をしていますが、 bind している先は先ほどと同じ BehaviorRelay です。

private(set) var article = BehaviorRelay<Article>(value: Article())

ここも上と同じですね。

articleDetailViewController に購読させます。

DetailViewController では以下のように購読させて自身の articlebind します。

これで詳細画面を表示することができました。

詳細

f:id:kosuke_1011:20180621150151g:plain

お気に入りの同期

TopViewControllerお気に入りした情報DetailViewController でも反映されます。

ただ、このままだと DetailViewController で変更した お気に入り情報TopViewController に反映されません。

なので、 DetailViewControllerお気に入り情報 の変更を先ほど ViewModel で実装した update に投げてみましょう。

まずは、お気に入りボタンのイベントを取得する必要があるので、以下のように subscribe します。

favoriteButton.rx.tap
    .subscribe { [weak self] _ in
        // 処理
    }.disposed(by: disposeBag)

これで subscribe できたので、あとは ViewModelupdate を呼ぶだけです。

guard let article = self?.viewModel.article.value else { return }
self?.viewModel.update(article: article)

これで、お気に入り状態は TopViewControllerDetailViewController で同期されるようになりました。

トップから詳細へ遷移

f:id:kosuke_1011:20180621150426g:plain

詳細からトップへ戻る

f:id:kosuke_1011:20180621150441g:plain

やってみて

MVVM+Fluxのメリット

・データが一方向にしか流れない点

・各 class の責務が明確なので複数人で実装をしてもそこまでブレない

MVVM+Fluxのデメリット

・Rxの知識が必要

・習得までのハードル

最後に

Wowma!のアプリ開発では Flux を採用していませんが、これを機に少しづつ採用してもいいかもと思いました。

反響があれば今回実装したリポジトリを別途公開しようと思います。

次回は Flux のライブラリの ReactorKit について書きたいと思います。