第2回エンジニアインタビュー!
プランニング・ポーカーって何ぞや!?工数算出にこんな手法が!
こんにちは!
前回に引き続き、プロジェクトマネジメント部の美保です。
本ブログの運営メンバとして、更新管理など担当しています。
エンジニアメンバーへの突撃インタビュー企画第2弾は
ダイレクト技術戦略部の座安朝秀さん(以下、Zさん)。
プランニングポーカーという開発手法について、お話を伺ってきました!
自分がやらなきゃ誰がやる!?
――Zさん、この度はインタビュー企画にご協力いただき、ありがとうございます。
まず、これまでの経歴や現在の業務内容について簡単にお話しいただけますでしょうか。
Z:2015年4月より旧LUXA社にJoinし、au WALLET Marketの立ち上げに携わってきました。
現在はLUXAサービスに関わるシステム全体の開発、アーキテクチャの策定等を担当しています。
チーム内の要件を取りまとめて、メンバーに指示を出す役割も担っています。
――10月からはグループリーダーとしてもご活躍されていらっしゃいますよね。
役割が増えた今、仕事上でモチベーションが上がるのはどんなときでしょうか?
Z:そうですね~…。。
自分がやらなきゃ誰がやる!?的な状況下では、気持ちが盛り上がります。
自身のミッションとして、サービスがスピーディーに開発できる体制、仕組みを作ることを目指しているので、
メンバーが良いプロダクトを素早く作れるような環境を作ることに全力を注ぎたいと思っています。
――素晴らしい!普段はクールな印象のZさんですが、熱い思いが伝わってきました!
そんな中、プランニング・ポーカー*1という手法を導入しようと思われたきっかけは何だったのでしょう?
プランニング・ポーカー合宿のはじまり!
Z:とあるプロダクトで、工数算出をしなければいけない状況があって。
これまで、自身が算出する工数と他人が算出する工数のギャップが激しいという
課題を抱えていたので、実際に手を動かして作業するメンバーとの擦り合わせが必要だと感じていました。
バッファを積みすぎる状況を回避したい、かと言って無理な工数も出したくないと思っていましたね。
――なるほど。そんな課題を一気に解決してくれるのがプランニング・ポーカーだったと。
具体的にはどんなやり方で実施されたのですか?
Z:別途社外会議室を貸し切り、他業務が入らないよう、丸1日合宿のような形式を取って集中的に行いました。
参加メンバーは合計で7名程度(自分の他に検証を実施するテストチーム+実際の作業を行うメンバー)です。
~やり方~
①まず、相対的な見積となるよう、ベースのタスクに対する工数(ポイント)を決めます。
②実施するタスクの工数(ポイント)がどれくらいか?各自でカードを提示します。
③提示ポイントに差があった場合、都度話合いをし、精査していきます。
上記①~③をタスク毎に繰り返し実施します。
1タスク5分程度でサクサク進むかと思っていましたが、
初回ということもあり、1タスクにつき15~20分程度かかりました。
メンバーの知識レベルにバラつきがあったこともあり、
実際は1日使って実施するつもりだった90タスクのうち、半分も消化できませんでした。。
――なんと…!活発な議論がなされた証拠ですね。実際やってみて、どんなメリットや課題が見えましたか?
Z:良かったのは、チームビルディングの一環として、メンバー間でナレッジの共有ができたことです。
今回テストチームも入ったので、テスト的な観点も工数見積もりに入れることができました。
また、自身がファシリテートしたことで、特定のメンバーが発言し続けるなどの偏りはなかったです。
改善点については、メンバーに対しての事前知識のインプットができていなかった点でしょうか。
思いの外、事前共有に時間がかかったため、プロジェクトの全体概要を予め伝えておけばよかったです。
(概要、要件説明に午前中丸々使ってしまい)実質のプランニング・ポーカーは午後から2~3時間の実施となりました。
――対象プロジェクトに対して、全員が近いインプットを持って臨めるとベターということですね。その他、振り返ってみていかがでしょうか?
根拠のないバッファがなくなり、個々の生産性が読めるように!
Z:プランニングポーカーで出す工数は標準的な工数になってきます。
ベテランの工数、新卒の工数。属人化した工数でなく、作業に対しての純粋な工数になってくるはずと考えています。
(ベテランになればなるほど、バッファ積まれがち。笑)
また、裏の目的として「個人個人の生産性が図れるようになる」というのがあります。
(標準的な工数に対して早く終われる人は生産性が高い等)
これらを数値化して、業務委託の方の評価に出せればよいなぁと考えています。
既にフロント側でも実施していますが、今後も推進していきたいです。
――個人の生産性を図れるようになる、という副次的な効果まで…!是非全社的に取り入れたい手法だと感じました。最後に、今後さらにやっていきたいことがあれば教えてください。
Z:ナレッジを共有する仕組みをもっと取り入れていきたいと思いますね。
旧LUXAのナレッジはドキュメントが混ざっている状態なので、
旧KCF分も合わせて有効活用できるようにしていきたいです。
aCLとして合併したので、お互いの良いところを共有、活かせるような仕組みを今後も作っていきたいと考えています。
――良い部分を組み合わせて、さらなる効率化を目指していきたいところですね!本日は、お時間いただきありがとうございました!
Z:ありがとうございました。
型にとらわれず、常に革新的なアプローチで開発メンバーをリードし続けるZさん。
プランニング・ポーカーは、単なる工数見積もりにとどまらず、
チーム間での意識の擦り合わせやプロダクトの品質を保つ意識を各自が認識できる
大変有意義な手法だと感じました。
次回のインタビューもお楽しみに!
*1:プランニングポーカーとは、「1、2、3、5・・・」といった数字が書かれたカードを使って、タスクの規模を相対的に見積もる手法。まず、作業を行うメンバーを集めそれぞれにカードを配り、その後それぞれのタスクに対して開発を担当するメンバーが思いのままに数字のカードを出し合う。
第1回エンジニアインタビュー!
クーポンTエンジニア佐藤さんにアレコレ聞いちゃいました
はじめまして!
auコマース&ライフ*1Wowma!プロジェクトマネジメント部の美保です。
本ブログの運営メンバとして、更新管理など担当しています。
弊社エンジニアメンバーへの突撃インタビュー企画ということで、
記念すべき第1回として
システム本部Wowma!システム開発部プラットフォーム開発グループの佐藤直人さんにお話を伺ってきました!
コーディングに夢中になりすぎて、いつの間にか朝になってることも。(笑)
――この度はインタビューにご協力いただき、ありがとうございます。
まず、出身地、休日の過ごし方などを教えてください。
佐藤:新潟県の佐渡ヶ島(偶然にも筆者と同郷なのです!)に、高校卒業まで在住していました。
中学生の頃から漁船に乗って働いていて、当時は毎朝4時起きでしたね。。
今となっては完全逆転生活です。笑
週末は深夜1時頃から個人学習でコーディングをしているのですが、つい夢中になってしまって。。
気が付くとMacの壁紙*2で朝になったことに気が付く場合もあります。笑
――なんと・・・!朝まで個人学習、立派すぎます!!感涙
佐藤:いえいえ。趣味は中高やっていた卓球で、今でも市民体育館にて練習を重ねる日々です。
社外に仲間がいて、試合も出ています。(シングル、団体戦)
サービス規模の大きさが、学びやモチベーション維持に繋がっている
――auコマース&ライフ(以下、aCL)に入社する前にやっていたこと、入社のきっかけについて教えてください。
佐藤:前職は新卒で入社した企業で、主に求人サイトの開発、運営を担当していました。
時にはメルマガを書いたり、Facebookの更新をしたり…、開発業務以外も幅広く対応していました。
入社のきっかけは、他の会社のサービス、業種を見てみたいという思いや、
もっとエンジニアとして成長したい気持ちが強かったためです。
――今、aCLではどんな仕事を担当されているのですか?
佐藤:au Wowma!(エーユーワウマ)*3のクーポンシステムを担当しています。
現在エンジニア:2名、プロジェクトマネージャー:1名、プロダクトマネージャー:1名と、
個性豊かなメンバーで構成されています。
いい意味でいろんなことを言い合える、仲が良いチームです。
毎週水曜日にスプリントレビュー*4を実施していて、技術共有や意見交換を行っています。
――確かに、クーポンチームはいつも笑いが絶えない、賑やかなチームのイメージです!
佐藤:はい、そうですね!
初めてECを経験しましたが、トラフィック*5も多いサービスなので、
普通なら影響がないコードでも、注意が必要な場合が多いです。
この点が非常に勉強になっています。
また、気が付けば社会人4年間のうち、2年半くらいリプレイスをやっています。笑
規模は異なりますが、大変さは変わらないですね。。
――大変な作業が続く中、ご自身のモチベーションをどのように維持されていましたか?
佐藤:そうですね~…。確かに大変なことは多いのですが、
やっぱりシステムを作るのが好きなので。
最近はあまり意識していなかったですが、世に出ていくものを作れるということは楽しいです。
(今担当している)クーポンはユーザーに直接利用してもらうことができ、
大きなサービスなのでやりがいを感じられる場面が多いですね。
――サービス規模の大きさがやりがいに繋がっているということですね。
ご自身の中でのミッションは何だと思いますか?
佐藤:店舗さんも含めて、より使いやすいau Wowma!のプラットフォームを作ること!
それから、安定してクーポンのシステムを運用していくことです。
エンジニア間の社内交流を深めて、技術を向上させたい
――佐藤さんの考えるaCLの魅力は何ですか?
佐藤:労働環境が良い点です。
働き方の自由度が高く、自分の時間が作りやすいですね。
――それはエンジニア職に限らずですね。私自身も感じている部分です。
逆に、今後変えていった方が良いと思う点も教えてください。
佐藤:社内交流、特にエンジニア同士の交流の場がもっとあってもいいかもしれないなと思います。
たまに社内勉強会やボードゲーム部の活動もあるのですが、頻度を上げたいですね。
――確かに、合併後エンジニア同士の交流の場がなかなか作れていないのは事実なので、
これからどんどん増やしていけたらと思います。
最後に、今後aCLで実現したいこと、チャレンジしたいことを教えてください。
佐藤:先ほどの話にも通じますが、自ら積極的に社内交流を深めていきたいと思っています。
そして、実務で使える技術をどんどん磨いていきたいです。
――学ぶ姿勢を常に持ち、前進し続ける姿が素晴らしいです。
本日はお時間いただき、ありがとうございました。
佐藤:ありがとうございました!
ご多忙な中、約1時間のインタビューに答えてくれた佐藤さん。
エンジニアとしてのスキルアップ、サービスへの探求心を
常に持ち続けている姿勢に、筆者自身も刺激をもらうことができました。
改めて佐藤さん、ありがとうございました。
次回のインタビューもお楽しみに!
Javaの静的解析ツール「PMD」を導入してみた件
※KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。 本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。
導入した背景について
こんにちは、KCFのエンジニアの坂本です。
今回は、静的解析ツールの導入の話です。
静的解析ツールといえば「どうでもいい細かいコードスタイルとか怒っていてウザい」と感じていませんか?
少なくとも、私はそうでした。
しかし、私たちのプロジェクト・チームの色いろな事情もあり、
「人間の眼によるレビューだけでなく、機械による自動的なチェックにも助けてもらう、必要がある。」
ということを痛感しました。
そこで、静的解析ツールを導入してみようということになったわけです。
今回はそんな話です。
静的解析ツールも色いろと種類があるわけで、
候補になりそうなものをざっとひと通り調べて見たのですが、
Javaの静的解析ツール「PMD」を採用することにしました。
導入の理由ですが、カスタムの設定が比較的シンプル・簡単に行えて、
自分たちのプロジェクトに合った設定を実現できそうだったからです。
例えば、極端な話、以下のようなカスタム設定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
ちょっとこれは「追記」になるのですが、
開発IDE(EclipseやIntelliJなど)の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クラス)がメンテナンスされています。
上に掲げたドキュメントを見てもらうと、分かるのですが、
それぞれのチェック項目には
■カテゴリーと優先度
という分類があります。
例えば、さっきの
「不要な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の単体テストについて
- はじめに
- 背景について
- build.gradleやH2とFlywayの設定など
- @SpringBootTestとConfigurationクラス
- coreサブプロジェクトのServiceとRepository
- さて本題の単体テスト
- つまづいた点(指定のH2が立ちあがらない場合)
※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であれば、別の単体テスト用のデータベースを立てる必要がないので便利ですね!
H2は起動時に動作モードをMySQL/PostgreSQL/Oracleなどで指定してエミュレートの指定が出来ます。
また単体テスト実行時のスキーマやテスト・データの管理は
■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というプラグインを使うと記述が楽そうだったのでこちらも一緒に利用してみました。
そちらも絡めて軽くレポートさせて頂きます。
インストール手順
基本的に公式サイトにドキュメントへのリンクがまとまっているので、インストールからデモ動作まではこちらから各種ドキュメント見て頂くのが良いと思いますが、ざっと説明しておきます。
今回は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
もう少し凝ったテストをしてみる
実際のシナリオテストとなるともう少し凝ったことをするケースがあるので、以下をやってみました。
これをテストケースとして記述してみます。
*** 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にイメージ登録!
※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を追加し、設定をしてきます。
といってもName以外はDocker Host URIが最低限設定できていれば問題ありません。
設定値はCentOSであれば
unix:///var/run/docker.sock
になると思います。
認証情報の設定
ECRにイメージ登録をするためのアクセスキーの情報を、Jenkinsの認証情報として登録します。
認証情報を登録する際は、認証情報をジョブから利用できるスコープが選択できます。 フォルダから認証情報のページに遷移すると、『Stores scoped to フォルダ名』のような項目があるので、 スコープ設定をしたい場合は、そこから遷移した先で認証情報を追加すると良いです。
認証情報の追加で、AWS Credentials
を選択し、先程のアクセスキー情報を設定してください。
フリースタイルジョブの作成
いよいよジョブ作成です。
まずはソースコード管理に、作成するコンテナイメージを作るDockerfileが記載されたリポジトリを指定します。
次に、ビルド項目の『ビルド手順の追加』からBuild / Publish Docker Image
を選択します。
『Build / Publish Docker Image』は以下のようになっているので、高度な設定ボタンを押下し、すべての項目を表示させてください。
各項目に設定する値は以下のようになります。
項目 | 値 |
---|---|
Directory for Dockerfile | コードリポジトリのルートからたどってDockerfileのある場所 |
Docker registry URL | https://ECRリポジトリのURI |
Registry credentials | ECRリポジトリの存在するリージョンの入ったキー情報 |
Cloud | システム設定のクラウドで追加したDocker設定の名前 |
Image | ECRのプッシュコマンドで指定されるタグ名 |
Push image | チェックする |
Registry Credentials | 上記のクレデンシャルと同じ |
これらの値を設定した画面が以下です。
これで設定は完了です。
あとはこのジョブを実行すると、コンテナイメージが作成され、そのイメージが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 Gateway、AWS Lambda、Amazon DynamoDBの構成で作る
RESTfullなAPIをサクッと作成していきます。
- 今回利用するツールの紹介
- 準備
- ローカルでアプリケーションの起動
- Typescriptの設定
- Lambdaで呼び出すコードを作成
- サンプルデータを作成
- DynamoDB Localをインストール
- DynamoDB Localを起動
- ローカルでアプリケーションを起動
- ブラウザからアクセス
- 記事を登録してみる
- ブラウザで確認
- 記事を削除する
- AWSへdeploy
- 所感
今回利用するツールの紹介
まず今回利用する各サービスを紹介します。
API Gateway とは
APIの作成と管理が簡単にできるサービスです。
どのようなスケールであっても、開発者は簡単に API の配布、保守、監視、保護が行えます。
AWS Lambdaで実行される処理の玄関として振る舞うAPIを作成できます。
AWS Lambda とは
何かしらのイベントによって処理を実行する環境です。
イベントとはAWS上のS3にファイルをアップロードや特定のエンドポイントにアクセス
といった何かしらのアクションのようなものです。
このアクションをトリガーに処理を実行することができます。
Amazon DynamoDB とは
フルマネージドなNoSQLデータベースです。
フルマネージドとは運用をAWSにおまかせでき
利用者はOSやMiddlewareのことを意識する必要がありません。
また、高い拡張性、データへの高速アクセスが可能で
AWS Lambdaとの連携も簡単に行うことができます。
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
ユーザーの追加をクリックします。
ユーザー名を入力し、プログラムによるアクセスにチェックを入れます。
そして「次のステップ:アクセス権限」をクリックします。
ポリシーのフィルタへ「AdministratorAccess」と入力し「AdministratorAccessへチェックを入れます。
そして「次のステップ:確認」をクリックします。
入力内容を確認し「ユーザーの作成」をクリックします。
作成された「アクセスキーID」と「シークレットアクセスキー」をメモします。
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 CLIにAWSのアカウント情報を紐付ける
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
に登録したデータが確認できます。
記事を登録してみる
$ 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/
記事が登録されていることを確認できました。
記事を削除する
$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X DELETE -d '{}' http://localhost:3000/api/items/2/delete
idが2の記事が削除されたことが確認できました。
AWSへdeploy
作成したアプリケーションを以下のコマンドでAWSへデプロイできます。
$ yarn run sls deploy -v
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/
記事を登録してみる
$ 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
実際に記事が登録されていることを確認できます。
実際にAWSで確認するとAPIが作成されていることを確認できます。
https://ap-northeast-1.console.aws.amazon.com/apigateway/home?region=ap-northeast-1#/apis
所感
- 馴染みのある
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)
- JavaScript(jQuery)
- 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に登壇してきました!
こんにちは。
iOSとAndroidの開発を行なっている高橋(@KoH_1011)です。
8月30日~9月2日、早稲田大学にてiOSDC Japan 2018が開催されました。
iOSDC JapanはiOS関連技術をコアのテーマとした技術者のためのカンファレンスです。
今年は3回目の開催になるそうで、3日+前夜祭の3.5日開催と年々規模が大きくなっていて、みなさんのiOS愛を感じます!
※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証明書の無視
- Java × Spring Boot のこと
- Spring Bootのlocal環境でのHTTP/HTTPSのProxy設定
- RestTemplateの場合はProxy設定が効いている
- RestTemplateBuilder・RestTemplateCustomizerを使う場合
- 付録:OpenId4JavaのProxy設定
※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」を使う場合には注意が必要です。
この有益なサイトの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
というものがあります。
今わざわざこれを選ぶという人がどれだけいるのか分かりませんが、
私たちのチームは今回は様ざまな事情から、
このライブラリを使う必要がありました。
そしてここでも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現場でよく交わされる言葉の意味解説になります。
よくある例と間違った例を両方のせることにより、理解を深めることを目的としています。
とにかくよくわからない言葉たち
アグリー
これは頻繁に聞く機会があるのではないかと思います。英語がわかる方ならすぐにピーンとくるでしょうね。
まずは、実際の使用例をみていきましょう。
よくある例
新人のシンジ『私は今回、こういう考えで進めていこうと思います』
部長『あー!うんうん!私もそれアグリー!!』
※何故か怒っている人
ということですね。
つまり簡単に言うと同意した時に使っておけば大丈夫です。英語のagreeからきているんですね。
間違った例
なんらかの伝承者『貴様はもう死んでいる』
部長『あー!うんうん!私もそれアグリー!!アグアグアグアグリィィォ!!!』
死んでます。
アサイン
勘弁してくれ。そう思った方も多いでしょう。また数学か、サインコサインタンジェントか。違います。これも使用例をみたら簡単にわかります。
よくある例
部長『あの炎上しているプロジェクトがあるじゃろ。今度君をあそこにアサインすることにした』
新人のシンジ『』
わかりましたね。
危険なプロジェクトに任命される時によく使われる言葉です。
配属とか任命とか、そういったわかりやすい言葉を使わずに、アサインといった西洋かぶれになる事で、煙に巻こうとしているんですね。こちらも英語のassignからきています。
間違った例
スネオ先輩『君、ここのコードどうなってるの?ねえ?いい加減にしてくれないかな???それにこのエディタ一人用だから君のぶんはないよ』
新人のシンジ『っせえぞ!!ネチネチしやがって!てめえの頭を目の前のモニタにアサインしてやろうか!?』
?!
マガジンじゃないんだから。
幸いにもKDDIコマースフォワードはお互いを尊重し合っているので、こんな会話は生まれません。
ファンダメンタル
いきなり難易度があがりましたね。
メンタルはなんとなくわかる方も多いでしょうが、ファンダとは…?パンダと言いたくて噛んでしまったのか?パンダメンタルなのか?
そんなわけがない。
とにかくここも例をみてみましょう。
よくある例
部長『とにかくファンダメンタルが重要なんだよ!まず、ファンダメンタルをおさえていかないとダメだよ』
とにかくメンタルは強い原田『お言葉ですが…僕はメンタル強いですよ。パンダは超えてます』
バカか
パンダは超えてますって、パンダのメンタルどんだけやねん。
これはちょっと勘違いしてますね。
ファンダメンタルは基本的なとか基礎的なとかそういった意味で使われます。
つまりこのケースですと、基本が重要だから、まず基本をおさえよう!と部長は仰られているのですね。
間違った例
とにかくメンタルは強い原田『お前らも俺のファンダメンタル見習ってさ、とにかくメンタル強くなろうぜ!世の中ファンダメンタルだよファンダメンタル!』
確かにここまで言い切れるのはメンタル強い。
リスケ
異常な頻出頻度を誇るこのワード。
IT業界に身をおいたことがあれば一度は聞いたことがあるでしょう。
馴染み深い、でもわからない。しかし使用例をみたら簡単に理解できてしまいます。
よくある例
新人のシンジ『申し訳ありません。こちらのリリース日をリスケさせていただけないでしょうか!』
部長『おっけー!』
お見事!真摯な態度で接することによって、中年男性の肩より凝り固まった部長の牙城を崩しましたね。
そうなんです、リスケとはリスケジュール(reschedule)の略で「計画を変更する」とか「スケジュールを組み直す」時に使われます。
間違った例
新人のシンジ『部長!この食べ残しのリスケ貰っちゃっていいですか!』
部長『おっけー!』
ビスケットじゃねーよ。
サマリー
長かったこの記事もいよいよ最後となりました。
これも非常に出てきやすい単語ですね。
サマリーだかサラリーだかわかりませんが、特に打ち合わせ時に出てきやすいものとなっています。
よくある例
会議の達人カワモト『ねえ、このミーティングの目次は?あと前回のサマリーないの?サマリー』
新人のシンジ『さ、サマリーですか...?』
というわけなのです。
ここからわかるように、サマリーとは「概要」とか「要約」「まとめ」といった意味なんですね。前回のまとめないの?そんな気軽な気持ちでカワモト氏は聞いていたのでしょう。これからサマリーという言葉が出てきても焦らずに対応することができますね。
そうです、答えは「サマリーはありません」です。
間違った例
会議の凡人ヤマゼン『君さ、そういうところがサマリーなんだよね。サマリー!わかる?』
新人のシンジ『わかるかい!』
完全に意味を理解せずにファッション感覚で使ってますね。
まとめ
はい、というわけで今回は新たにビジネス用語を5つも覚えてしまいました。
これらを使いこなせば、まわりからは一目置かれる存在になること請け合いです。
最後に今日の復習として、5つ全ての言葉を取り入れてよくある例を提示して終わります。
よくある例
新人のシンジ『今回の会議のサマリーはこちらとなっており、重点的にお話したい部分はスケジュールのリスケについてです』 とにかくメンタルは強い原田『すげーじゃん!まじファンダメンタルだよ君!!』 部長(なんだこいつ頭おかしいのか...) 新人のシンジ『ありがとうございます。リスケの理由としてはこれこれで、申し訳ありませんが承認をいただけないでしょうか』 スネオ先輩『その理由じゃ仕方ないね、アグリーです』
部長(いや理由って「これこれ」でなんでわかるんだこいつ...)
部長『それって人を追加でアサインすることで解決したりはしないの?』
新人のシンジ『あ、アグリーじゃないんですか...?サマリーもリスケも使って説明したのに!?アアアア!!』
部長『どうしたんだ!落ち着きたまえ』
とにかくメンタルは強い原田(全然こいつファンダメンタルじゃなかったわ。やはり俺こそがファンダメンタル)
新人のシンジ『アガアガアアガアガアアアアアア!!!アアアアアアアアアアァァァァァァァ!!!!!』
はい、無理にカタカナ用語を頻発するとメンタルに影響することが多いですね。
カタカナ用語は用法容量を守って正しく使用しましょう。
言葉なんてね、意味が伝わって相互に理解できるのが一番なんです。
無理にカタカナ用語で喋らなくても、自然体で喋るのが一番ということですね。
※今回の登場人物は全て架空であり、実在致しません。
webpack-dev-server後継のwebpack-serveを利用してみたお話
2018.09.20 追記: webpack-serveは DEPRECATED となりました。 これまで通り webpack-dev-server を利用することをおすすめします。
はじめまして。エンジニアの白本です。
皆さんはフロント開発における開発用ローカルサーバは何を利用されていますか?
browser-sync?webpack-dev-server?
私は少し前からwebpack-dev-serverの後継であるwebpack-serveを利用しています。
※KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。 本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。
webpack-serveってなに?
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.jsonにJSON形式で書くこともできます。
..., "serve": { "port": 3000, "reload": true, "config": "webpack.config.js", "content": "./public", "devMiddleware": { "publicPath": "/assets" } } ...
3.JSONやYAMLファイルに書く
2で書いた内容をJSONやYAMLファイルとして書くこともできます。
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に書く
こちらもJSONやYAMLと同じで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を利用したいくつかのパッケージも既にあるようです。
おわりに
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です。
※ハワイ太郎として四人目の太郎を狙っています
突然ですが...!
今、私は猛烈にお腹が張っています
何故ならお昼にペヤ◯グGIGA MAXを食べてしまったからです。
知ってます?これに使うお湯の量って1.3Lですよ!?
150杯くらい食べたら、湯船に浸かるのと同じレベルになっちゃいますね!
実際これを食べた直後はヘブン状態*1に陥っていて、思わず歌を口ずさむ始末です。
ペ◯ング それは 君がみた白い箱
ぼくが 食べた カロリー
ペヤ◯グ それは 混ざらないソース
幸せの 茶色いそば ◯ヤング
ジーーーンときちゃいましたね。
さて、ここまでで十分Wowma!アプリチームの雰囲気は感じ取れたかと思います。
というわけで、今回はそんなチームの歴史に関して振り返っていきましょう。
チーム結成
私が入社したのは2017年の10月頃。
前職の有休消化中は職場*2の雰囲気に馴染めるかどうかだけが気になり、入社時の挨拶を1日中考えていました。
もっとも考えた挨拶は時事ネタが多かったので、結果的に直前に考え直すという全く意味のない時間を過ごした時期でもありました。
まあ人生、意味のない時間の方が多いと思っておりますので、そこは問題なかったのですが、問題だったのはむしろ入社後にチームがまだ存在していなかったということでしょうか。
そんな中、部長の「これからアプリを推していくんだから、専用チーム必要だよね!?」*3といった一声でアプリプロダクトとしてのチームが結成されたのでした。
めでたし、めでたし
カンフー映画かよ!
気を取り直しまして...さて当時、どんなメンバーがいたのか気になるのではないでしょうか。
しかし、初期メンバーは...大変申し訳ありません。
私の口からのべることが大変難しい状況です。
本当に申し訳ありません。
やはり、メンバーにもプライバシーといったものがございまして、許可を得ずに勝手に書くわけにはいきません。
m(_ _)m
...
..
.
イカれたメンバー 紹介するぜ!
まずは、青◯のYontaro
以上だ!
苦難のデイリー
そんなこんなでチーム結成にいたった私たちですが、やはり出てくるのは毎日の情報共有問題です。
ここは先人の知恵を拝借しよう、ということでデイリースクラムを画策した我々ですがそこはご存知の通り、決まった時間にくるのが苦手な私ですから、必然的に開催が危ぶまれます。
当初10時から開催されていたデイリースクラムも、開始時間が1時間ずつ遅れていき、最終的に夕方の会議へと進化。更にはその夕会までも消滅。
なんでこうなってしまったのか?
それを把握するべく、脳内で振り返りを行ってみました。
※振り返りなのにお金のことしか考えていない人
な〜んだ
振り返るまでもない。非常に簡単なことでした。
「決まった時間に人(主にYontaro)がいない」
時間を守って打ち合わせを行うというのは、人生において一番大事なことの一つかもしれません。
と、これだけで終わってしまうと、どうしようもない人が集まっているチームといった印象を与えてしまうので
勿論それだけではないことを伝えておきたいです。
毎日・毎週など決まった時間に実施される会議は基本的に形骸化してしまう。
それは何故なのか?誰もつきとめることのできなかった謎を解明しようとして命を落とした若者が
当時「興味を引かぬものは意味がない」と言いきり、カルト的人気を集めていたテイレ=イ=ミナシだ。
彼は息をひきとる直前に、目的もなく決まった時間に行う会議は意味がないと断言し、安らかな笑顔でこの世を去った。
人々が疑問に感じていた、定例会議時のもやもや感を言語化した彼の偉大な功績を讃えて「内容のない定例会議は意味がない」と現代まで伝えられている。
民明書房刊『世界の打ち合わせ達人伝』より
そうなんです、つまり会議の内容が悪かった。
私たちも人間ですから、面白くないものを毎日ダラダラとやりたくないわけです。
それは打ち合わせも然り。
まあ、今思えばデイリースクラムの内容に問題があったんですね。
だけど当時の私たちはそこに思いを馳せることなく、こりゃアカンとなり、PDCAでいうところのActionで
朝会の終了を選択してしまったのです。
これはいってみれば、しりとりでいきなり「ん」を使って終わらせてしまったようなものでして、
おいおい!それって終わっちゃうじゃん!しりとりって文字を繋げていくんだよ!
こういった基本も怠って、改善活動の「か」の字も行わずに終了へと持ち込んでしまいました。
この時の反省が今になって生きているというのはありますね。
振り返り〜そして消滅?
さて、デイリーすら続かないチームとなってしまった我々。
巷ではレトロスペクティブと呼ばれている振り返りはどうなのでしょうか?
スクラムとか関係なく振り返りって大事だよね!KPT*4だよね!
そう日頃から信念をもって生活していた我々は毎週振り返りを行っていました。
しかし、折角だしたTryが全く実践できない。
そんな毎週毎週KEEPなんてでねーよ!などといった心の声を聞きつつも、懸命に運営を行っていましたが断念。
※間違った振り返り
原因の一つは人数が多すぎて時間がかかりすぎたことにあると考えています。
当初数名からはじまっていたアプリチームも、振り返りを断念したタイミングでは10名以上に増加していました。
三人寄れば文殊の知恵とはいいますが、集まりすぎても非効率なんですね。
なので我々は苦渋の決断を下したのです。
そう、振り返りをネイティブアプリを行うチームとアプリAPIを取り扱うサーバチームの二つに分割してしまったのです。
これが功を奏したのか、私が不参加を決めたことがよかったのかわかりませんが、なんとこの振り返りは半年以上たった今も続いています。
いやあ、振り返りって本当にいいものですね。
現在
そんなこんなで波乱万丈なチーム人生を送ってきたアプリチームですが、現在はエンジニアを10名以上抱え、チーム全体では16名ともなる巨大なチームへと成長しました。
最近では新しいスクラムマスターの方も入り、和気藹々としながらも、日々真剣にお客様へ感動を与えるべくアプリの開発を行っております。
前半とはうってかわって後半突然真面目に紹介し始めるとは、書いている本人も思いませんでした。
やはりおかしな人を演出しようとしても、人間本来の性質が出てしまう。
昔の偉い人も言っておりました。
なので現在のチーム状況を語る際に真面目になってしまうのは、いわば必然であります。
そうなってくるとおそらく気になるのは、チーム構成でしょう。
書いていいのかわかりませんが、これが世に出ているという事はOKがでたということでしょう。
イカれたメンバー構成※2018年7月現在
▼エンジニア
ネイティブアプリ:5名
サーバサイド:5名
▼デザイナ
Pythonマニア: 1名
▼PO/SM
PO: 1名
SM: 1名
▼その他
ウィスパーMatsuno: 1名
レビュースペシャリスト: 1名
Yontaro: 1名
といった完璧な布陣!
ちゃらおのスクラム日記
KCFでプロダクト開発をスクラムで回してる神保です。
今回はスプリントの終了と始まる前に行うレトロスペクティブについて少しお話を・・・。
※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かけてレトロスペクティブのイベントを回してた。
んで、今回は手法を変えてみた。 ルールは以下
①スプリントの気分を表現する
- スプリント大変だった?
超大変 ↔ ちっとも
- スプリント楽しかった?
サイコーに楽しい ↔ ぜんぜん
- 次はもっとうまくできそう?
できる ↔ もう限界
↑の三点を各質問毎にチーム全体で横一列に並んで表現してみる。
②付箋に変えたいことを書く個人で書いて共有する
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を試してみた
こんにちは。
現在、iOSとAndroidの開発を行なっている高橋洸介(@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は以下の図でよく説明されています。
図でわかるようにデータが単一方向に流れるので非常に見通しがよくなるほか、「データはすべて Action
を介して更新する」という制約があるため、 View
の更新状態を予測しやすくなります。
それぞれの責務を説明すると、
・Dispatcher:発火された Action
を Store
に通知するもの
・Store:dispatcher
経由できた Action
を格納するもの
・Action:View
等から発火されたイベントを dispatcher
に通知するもの
・View(ViewController): Store
のデータを反映するもの。イベントを Action
に通知するもの
開発環境
・Xcode:9.4
・Swift:4.1
仕様
QiitaAPIを使って以下のことを実現させていきます。
・ViewController
に記事がリスト表示されている。
・DetailViewController
にお気に入り状態が表示されている。
・ViewController
と DetailViewController
のお気に入り状態が同期されている。
使用するライブラリ
まずは使用するライブラリを取り込みます。
使用するライブラリは実際にWowma!アプリでも使用している以下のものを使っていきます。
・APIKit
・Himotoki
・RxSwift
Model
Himotoki
を使って Model
の生成をしていきます。
今回はお気に入り状態の更新を見ていきたいので、 title
と favorite
の2つを定義しています。
Request
APIKit
を使って Request
の生成をしていきます。
リクエスト先は (https://qiita.com/api/v2) になります。
このAPIで新着記事一覧を取得できます。
Flux
Fluxには以下の要素が必要なので、実装していきます。
・Action
・Dispatcher
・Store
Action
まずは Action
の実装をしていきます。
今回の仕様は
・
ViewController
に記事がリスト表示されている。・
DetailViewController
にお気に入り状態が表示されている。・
ViewController
とDetailViewController
のお気に入り状態が同期されている。
なので、 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
で必要な情報は Dispatcher
で dispatch
した PublishSubject
を購読して処理に移すことです。
また、購読した値を View
に反映させる責務もあるのでその実装もしていきます。
購読するものは Dispatcher
で実装した articles
と article
になるので、まずは初期化メソッドにて 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
の更新に必要な articles
と article
を定義します。
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つですね。
articles
を TopViewController
で購読させます。
ViewController
では以下のように購読させて自前で作成した TopDataSource
に bind
します。
長くなりましたが、これで記事の一覧を表示することができました。
お気に入りもできます。
viewModel.articles .asObservable() .bind(to: tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag)
記事一覧
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())
ここも上と同じですね。
article
を DetailViewController
に購読させます。
DetailViewController
では以下のように購読させて自身の article
に bind
します。
これで詳細画面を表示することができました。
詳細
お気に入りの同期
TopViewController
で お気に入りした情報
は DetailViewController
でも反映されます。
ただ、このままだと DetailViewController
で変更した お気に入り情報
は TopViewController
に反映されません。
なので、 DetailViewController
の お気に入り情報
の変更を先ほど ViewModel
で実装した update
に投げてみましょう。
まずは、お気に入りボタンのイベントを取得する必要があるので、以下のように subscribe
します。
favoriteButton.rx.tap .subscribe { [weak self] _ in // 処理 }.disposed(by: disposeBag)
これで subscribe
できたので、あとは ViewModel
の update
を呼ぶだけです。
guard let article = self?.viewModel.article.value else { return } self?.viewModel.update(article: article)
これで、お気に入り状態は TopViewController
と DetailViewController
で同期されるようになりました。
トップから詳細へ遷移
詳細からトップへ戻る
やってみて
MVVM+Fluxのメリット
・データが一方向にしか流れない点
・各 class
の責務が明確なので複数人で実装をしてもそこまでブレない
MVVM+Fluxのデメリット
・Rxの知識が必要
・習得までのハードル
最後に
Wowma!のアプリ開発では Flux
を採用していませんが、これを機に少しづつ採用してもいいかもと思いました。
反響があれば今回実装したリポジトリを別途公開しようと思います。
次回は Flux
のライブラリの ReactorKit
について書きたいと思います。