とあるスクラム開発チームの振り返り

この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2015 の投稿記事です。

はじめに

この記事は、とあるスクラム開発チームのセレモニーを淡々と振り返るものです。
過度な期待はしないでください。

みさなまどうも。スクラムマスターぶらいじぇんです。

つい先日、『 英語サプリ 』 のWeb版がリリース1)iOSアプリは 10月29日、Web版が 11月30日 リリース。Androidアプリは今後リリース予定!(震えて待てされました!
『 英語サプリ 』は、英語の4技能「聴く・話す・読む・書く」のうち、日本人が苦手とする「聴く力」「話す力」の向上に特化したサービスです。英語を本格的に学習したい、英語を習得してグローバルに活躍したいという人はぜひ使ってみてください。無料で使いはじめることができます。

この記事について

この記事では、スクラムで開発をしている 英語サプリ の開発チームが

理解は容易だが、習得は困難である

といわれるスクラムをどのようにして習得してきたか(現在も習得し続けているか)について、スクラムマスターの役割を担っていた私の視点から、スクラムの5つのセレモニーをとりあげて振り返ります。

英語サプリWeb

振り返りの対象

  • 対象チーム:英語サプリ開発チーム
  • 対象期間:開発チーム組成からiOS版をリリースするまで( 2015年4月 〜 10月 )

スクラムの5つのセレモニーと、その振り返り

1.スプリントプランニング

スクラムガイドによると

スプリントが 1 か月の場合、スプリントプランニングのタイムボックスは最大で 8 時間である

と書かれています。
そのルールを正しく適用すると、我々のスプリントのタイムボックスは 1 週間ですので、最大で 2 時間ということになります。ですが、実際にはおよそ 1 時間と決めて時間内に収まるようにしています2)「1 時間」というのは、このチームが成熟した状態において適切な時間だと考えています。
このような取り組みを実施することができたのは、チーム組成の初期から取り入れていて、目的を満たせているかの確認は怠らなかったためです。
チーム組成の初期からあえてこうすることで「プロダクトバックログリファインメント」の重要性を認識し、事前にプロダクトバックログの項目を把握しておく習慣を身に付けてもらおうとしました3)「ミーティングの時間が足りなかったから、次回からミーティング時間を長くしよう」と考える人は少なく、たいていの場合は時間内に完了できるような工夫をするのではないでしょうか。

スプリントプランニングが終了するまでに、開発チームは自己組織化したチームとしてどのよう にスプリントゴールを達成し、どのように期待されるインクリメントを作成するかをプロダクトオー ナーとスクラムマスターに説明できなければいけない。

スプリントプランニングで重視しているのは、「受け入れ条件」を明確にすることに加えて、「どのようにレビューするか」までを想定しておくことです。こうすることによって受け入れ条件がさらに具体的になり、各メンバーがスプリントレビューのための準備をするようにもなります。

2.スプリントレトロスペクティブ

スプリントが 1 か月の場合、スプリントレトロスペクティブのタイムボックスは 3 時間である

1 週間のタイムボックスの場合は 45 分が適切ということになります。

スプリントレトロスペクティブが終わるまでに、スクラムチームは次のスプリントで実施する改善 策を特定しなければいけない。これらの改善策の実施は、開発チーム自体の検査の適応にな る。改善はいつでも実施可能だが、スプリントレトロスペクティブは検査と適応のための公式な 機会である。

スプリントレトロスペクティブで決め事としているのは、1 度のタイムボックスで必ず達成できる粒度の具体的なアクションの決定をアウトプットとすることです。このチームでは徹底しています。その理由は、『1 度のタイムボックスで必ず達成できる粒度(= 検証可能にする)』の『具体的なアクション(= ルールやスローガンにせずに、身に付ける)』を実施するためです4)私自身がルールを細かく覚えている自信がなく、都度ルールを確認するのは面倒だと思っていたたというのが理由でもあります。
とりあげられた内容によっては重要でなさそうなものもあり、その場合は「次のスプリントレトロスペクティブでもあがったら対応しよう」とする場合が多いため、45 分以内に終えることは難しくありません。5)「すぐに全てを解決する必要はない」という認識をチームがもつことでスムーズにいきます。

3.プロダクトバックログリファインメント

リファインメントは、開発チームの作業の 10 %以下にすることが多い

スクラムガイドにはそう書いてあります。プロダクトバックログリファインメントの時間は 1 週間のタイムボックスで 30 分ほど確保していました。
ここで「確保していた」と書いたのは、実際には実施しないことがあったからです。開発の進め方にもよるのですが、「開発チーム組成からiOS版をリリースするまで」の期間のプロダクトバックログは、限られた期間内にどれだけの機能を作れるかという考え方で作られていました。そのため、プロダクトバックログをリファインするよりはスケジュールを確認してリソースのやりくりをするために時間を使っていました6)先を見越して開発メンバーのフルスタック化を実施するなど、チームをリファインしていた。(笑
iOSアプリをリリース直後のスプリントからはスクラムの定義通りのプロダクトバックログリファインメントが実施されています。これはPOやチームがプロダクトバックログリファインメントの重要性を理解していることにもよりますが、振り返ってみると、「1 週間のタイムボックスで 30 分ほど確保していた時間」はチームがプロダクトバックログを見て、何かをリファインする時間として機能していたのではないでしょうか。そこで優先することが、リリース前は「スケジュールの確認」であり、リリース後は「プロダクトバックログリファインメント」になったのだと考えています。

4.デイリースクラム

デイリースクラムは毎日 10時30分 からチャットで実施しています。
毎回のレトロスペクティブで、「次のスプリントでのデイリースクラムはチャット上でのコミュニケーションを続けるか、対面(オフライン)でのコミュニケーションに切り替えるか」を決めています。それによって、チームは自分たちのスクラムセレモニーそのものを見直し続けることができています。

デイリースクラムは、コミュニケーションを改善し、その他のミーティングを取り除き、開発の障害物を特定・排除し、迅速な意思決定を強調・助長して、開発チームのプロジェクト知識のレベルを向上させるものである。これは、検査と適応の重要なイベントである。

このチームにとってのデイリースクラムは上記に加え、「時間を順守している」ことと、「常に最適な選択をしている」ことに自覚をもつ機会となっているのではないかと考えています。

補足
デイリースクラムは一般的に face to face でのコミュニケーションが良しとされています。
もちろん face to face での実施が理想なのですが、それをふまえた上でこのチームがデイリースクラムをチャットで実施し続けているのは以下の理由からのようです。
・リモートワークを導入していて、決まった時間に集まって話をするよりも普段のチャットでのコミュニケーションを活発にしているほうが効率が良い
・チーム全体を俯瞰してサポートしているエンジニア、スクラムマスターがいる
・ATI7)圧倒的当事者意識がある

5.スプリントレビュー

5つのセレモニーはどれも重要ですが、「サービスのリリースを目標にして開発を進めるチーム」にとって最も重要だったのは、スプリントレビューだと考えています。

スプリントが 1 か月の場合、スプリントレビューのタイムボックスは 4 時間である

1 週間のタイムボックスの場合は 1 時間が適切なのですが、1 時間以内に完了することはほぼありません( 1 時間 30 分ほどかかります)。スプリントプランニングで「どのようにレビューするか」までを想定しておきながら、時間内に収まらない理由は、スプリントレビューの場ではPOをはじめとして、メンバーからも新たなアイデアが出るからです。

スプリントレビューの成果は、次のスプリントで使用するプロダクトバックログアイテムが含まれた改訂版のプロダクトバックログである。新たな機会に見合うように、プロダクトバックログを全体的に調整することもある。

初期の頃からしっかりと取り組んでいたスプリントレビューが、実施する各セレモニーのなかで最も成熟していて、結果的に理想とする状態に近づいているようです。

まとめ

スクラムは理解が容易で習得が困難と言われていますが、この経験を通して、その言葉の意味を少なからず理解できた気がします。スクラムガイドではスクラムの定義や進行方法について説明されていますが、実施するために必要なものはメンバーの理解と意識(ATI8)圧倒的当事者意識です。
今回の開発においてはチームの組成からスクラムマスターとして役割を担っていたので、チームが成長するプロセスにおいていろいろな工夫をすることができました。ある程度成熟したチームのスクラムマスターを任されることになった場合にも、ここで得た経験はおそらく役に立つでしょう。どのような場合でもスクラムマスター自身が明確な考え方を持ち、スクラムの用語で説明できなければならないと感じました。

脚注   [ + ]

1. iOSアプリは 10月29日、Web版が 11月30日 リリース。Androidアプリは今後リリース予定!(震えて待て
2. 「1 時間」というのは、このチームが成熟した状態において適切な時間だと考えています。
3. 「ミーティングの時間が足りなかったから、次回からミーティング時間を長くしよう」と考える人は少なく、たいていの場合は時間内に完了できるような工夫をするのではないでしょうか。
4. 私自身がルールを細かく覚えている自信がなく、都度ルールを確認するのは面倒だと思っていたたというのが理由でもあります。
5. 「すぐに全てを解決する必要はない」という認識をチームがもつことでスムーズにいきます。
6. 先を見越して開発メンバーのフルスタック化を実施するなど、チームをリファインしていた。(笑
7, 8. 圧倒的当事者意識

ぶらいじぇん

Lv.
3
EXP.
556

スクラムマスターもやるサーバサイドエンジニア。 テストコードを書くのが好きで、趣味はリファクタリング。 仕事ではScalaでPlay Frameworkを使っているが、やっぱりRoRは便利だなぁと思う。

この執筆者の記事一覧

コメントはこちらのに同意の上、投稿ください。

  • binshi

    When I connect to VPC I get

    at com.google.inject.internal.MembersInjectorImpl.injectMembers(MembersInjectorImpl.java:132) [task/:na]

    at com.google.inject.internal.MembersInjectorImpl$1.call(MembersInjectorImpl.java:93) [task/:na]

    at com.google.inject.internal.MembersInjectorImpl$1.call(MembersInjectorImpl.java:80) [task/:na]

    at com.google.inject.internal.InjectorImpl.callInContext(InjectorImpl.java:1103) [task/:na]

    at com.google.inject.internal.MembersInjectorImpl.injectAndNotify(MembersInjectorImpl.java:80) [task/:na]

    at com.google.inject.internal.MembersInjectorImpl.injectMembers(MembersInjectorImpl.java:62) [task/:na]

    at com.google.inject.internal.InjectorImpl.injectMembers(InjectorImpl.java:984) [task/:na]

    at com.google.inject.util.Providers$GuicifiedProviderWithDependencies.initialize(Providers.java:149) [task/:na]

    at com.google.inject.util.Providers$GuicifiedProviderWithDependencies$$FastClassByGuice$$2a7177aa.invoke() [task/:na]

    at com.google.inject.internal.cglib.reflect.$FastMethod.invoke(FastMethod.java:53) [task/:na]

    at com.google.inject.internal.SingleMethodInjector$1.invoke(SingleMethodInjector.java:57) [task/:na]

    at com.google.inject.internal.SingleMethodInjector.inject(SingleMethodInjector.java:91) [task/:na]

    at com.google.inject.internal.MembersInjectorImpl.injectMembers(MembersInjectorImpl.java:132) [task/:na]

    at com.google.inject.internal.MembersInjectorImpl$1.call(MembersInjectorImpl.java:93) [task/:na]

    at com.google.inject.internal.MembersInjectorImpl$1.call(MembersInjectorImpl.java:80) [task/:na]

    at com.google.inject.internal.InjectorImpl.callInContext(InjectorImpl.java:1092) [task/:na]

    at com.google.inject.internal.MembersInjectorImpl.injectAndNotify(MembersInjectorImpl.java:80) [task/:na]

    at com.google.inject.internal.Initializer$InjectableReference.get(Initializer.java:174) [task/:na]

    at com.google.inject.internal.Initializer.injectAll(Initializer.java:108) [task/:na]

    at com.google.inject.internal.InternalInjectorCreator.injectDynamically(InternalInjectorCreator.java:174) [task/:na]

    at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:110) [task/:na]

    at com.google.inject.Guice.createInjector(Guice.java:96) [task/:na]

    at com.google.inject.Guice.createInjector(Guice.java:73) [task/:na]

    at com.google.inject.Guice.createInjector(Guice.java:62) [task/:na]

    at play.api.inject.guice.GuiceBuilder.injector(GuiceInjectorBuilder.scala:126) [task/:na]

    at play.api.inject.guice.GuiceApplicationBuilder.build(GuiceApplicationBuilder.scala:93) [task/:na]

    at play.api.inject.guice.GuiceApplicationLoader.load(GuiceApplicationLoader.scala:21) [task/:na]

    at example.PlayTask.WithApplication(PlayTask.scala:16) [task/:na]

    at example.PlayTask.exec(PlayTask.scala:9) [task/:na]

    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71]

    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71]

    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71]

    at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_71]

    at lambdainternal.EventHandlerLoader$PojoMethodRequestHandler.handleRequest(EventHandlerLoader.java:439) [lambda-sandbox.jar:na]

    at lambdainternal.EventHandlerLoader$PojoHandlerAsStreamHandler.handleRequest(EventHandlerLoader.java:370) [lambda-sandbox.jar:na]

    at lambdainternal.EventHandlerLoader$2.call(EventHandlerLoader.java:972) [lambda-sandbox.jar:na]

    at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:231) [lambda-sandbox.jar:na]

    at lambdainternal.AWSLambda.(AWSLambda.java:59) [lambda-sandbox.jar:na]

    at java.lang.Class.forName0(Native Method) [na:1.8.0_71]

    at java.lang.Class.forName(Class.java:348) [na:1.8.0_71]

    at lambdainternal.LambdaRTEntry.main(LambdaRTEntry.java:93) [runtime/:na]

    Caused by: java.net.UnknownHostException: ip-10-0-77-249: unknown error

    at java.net.Inet4AddressImpl.lookupAllHostAddr(Native Method) ~[na:1.8.0_71]

    at java.net.InetAddress$2.lookupAllHostAddr(InetAddress.java:928) ~[na:1.8.0_71]

    at java.net.InetAddress.getAddressesFromNameService(InetAddress.java:1323) ~[na:1.8.0_71]

    at java.net.InetAddress.getLocalHost(InetAddress.java:1500) ~[na:1.8.0_71]