社内での技術発表会を定期的に行うことで心がけていること

freee 株式会社 松崎 啓治です。 人事労務freeeのアプリケーション開発を担当しています🔥

この記事は、freee developers Advent Calendar 2017 20日目の記事です。

社内の技術発表会 真剣.js

真剣.jsという名前からしてJavaScriptを連想しますが、特に関係なく、技術的な発表を行う会です。 社員数が少ない時代から続いています。私がfreeeに入る前から、初期に入社した社員によって運営が切り盛りされていました。

f:id:syanbi:20171218200146j:plain
人数が少なかった頃

初期担当者が忙しくなり、切り盛り具合が大変になったため、私側で真剣.jsの名前を借り、技術勉強会を行うようになりました。

無理のない発表体制

技術発表会では、ざっくりと5分〜10分枠、15分枠を設けて、 発表したい人が居たらどのぐらいの時間枠で発表できるか選択した上で エントリーしてください、とお願いしています。

f:id:syanbi:20171219000042p:plain
社内で利用しているQiita::Teamに募集の投稿をした様子

話したい人がある程度居ないと技術発表会は会として成り立たないので、 4半期に1度のペースで技術発表会の開催を行えるように調整しています。

エンジニアの交流

2017年に入った後、エンジニアの人数も100名まで近づき、担当しているプロダクトや、得意分野、興味、家族構成、出身などかなりのバラつきが出るようになりました。 新しく入社した人と前から働いている人の技術的な興味に関するつながりはきっかけがなければ交わることがありません。

技術発表会、真剣.jsを通じて「ああいうことに技術は応用できないか」「私は運用を知っている」「もっとより良い手段を見つけている」など、 1名の技術興味、視野では追いづらいのを補完する作用があります。実際に機械学習系の発表をエンジニアが行ったところ、聴衆側のエンジニアが 自身の知識や、興味を元に質問をし、発表に出した抱え込んでいた課題の解決策を話し合っていたりしました。

開くことを定常にせず。なんとなくやっている技術発表会

開くことを義務とし、定常とし、発表者に義務を課しつつ開催し続けるスタイルにできるかもしれません。 が、それではなかなか長続きしないし、リラックスした形で発表はできないと思うし、 聴衆も聞くことが義務になるけど、興味のないことに耳は傾けづらいだろうし、レビューもしづらい。 モチベーションを維持しつつ、面白そうな発表や、質問があったらその都度気にしてもらえればいい、 そんな思いで続けています。

低コスト運用

f:id:syanbi:20171218201046j:plain

酒を発注したり、食べ物を発注したりして、コストをかけようとがんばったことも数回あったのですが、 ここ最近はやめ、場所を押さえる・発表者を集める・時間を予定する・リマインドする 以外のことには手を出さなくなりました。お酒があったり、食べ物があると人は集まりそうなのですが、 お酒があったら正しい判断しづらそうだし、発表を聞いた上で話が盛り上がったらより良い酒を飲みに行くとか、 そんな流れになったほうが良いかなという思いが強いためです。(本当は合ったほうがいいかなと最近思っては居ます)

コストが高くなると、開催する意図を細かく説明し、コストが係る理由を正当化していかなくてはなりません。 お酒・食べ物の消費も見積もらなければならないし、そうなると運営として1名ではなく複数名となるようになります。 そうすると適当な開催がしづらくなり、打ち合わせのためのミーティングが開かれ、ミーティングのためのミーティングが... となり技術発表会の開催の目的が業務の匂いが強いものになってしまいます。

なかなかそうすると紋切り型の発表が増えそうだし、一発ネタや時事ネタが流行りそうで、 交流や思わぬ発見があった、などのよくわからないおもしろイベントが発動する機会が減りそうかな〜とか そんなことを思うため、低コスト運用を心がけています。

長く、定期的に続ける

勉強会で得たノウハウや、知見共有や、そこで生まれた場や出会いなどがあるので、 もったいないので長く続けていきたい気持ちを持ってやっています。 真剣.jsをやる、という呼び込みをするとエンジニア以外の方からも真剣.jsにて発表したい、 ということで申し込みがある状態になっていて、非常にうれしいです。🙏

f:id:syanbi:20171218235539j:plain
社内への広報や宣伝に力を入れすぎなくても、自主的に集まり、技術発表が始まる

そして...

社内での技術発表会に参加したい、企画したいという方。 一度弊社まで遊びに着てください。ご案内いたします🔥 最近大阪オフィスでの採用も始まっているようですよ。

www.wantedly.com

エンジニアに限らず、ビジネス、サポートも募集しています。

jobs.freee.co.jp

次の担当は

明日、12/21のfreee Developers Advent Calendarはfreee エンジニア C#の要(かなめ)、@toshi0607 が担当します。 お楽しみに👏👏

ReactComponent のリファクタリング指針

エンジニアの id:t930 です。
freee Developers Advent Calendar 2017 19日目いきます。

React はその名前を聞くようになってから3年以上が経過し、Webアプリケーション開発の文脈においてはもはや枯れた技術と言えるでしょう。会計freeeでも2015年ごろに Backbone.js から React へのリプレースを行い、現在では Reactコンポーネントだけでも900近いファイルが存在しています。当然このような規模でやっているとリファクタリングも必要になってくるわけで、本記事ではそんな中で得られたReactコンポーネントにおけるリファクタリングの指針について紹介していきます。1

適切な単位に分割する

React に限った話ではないですが、巨大で見通しの悪いコンポーネントはメンテナビリティや再利用性の低下を招きます。表示領域、責務、意味付けに応じて適切な単位に分割します。

Bad

// state, method, rendererの肥大化したコンポーネント
class BigComponent extends React.Component {

  constructor() {
    super(...arguments);
    this.state = {
      //...
    }
  }

  handleClickFoo() {
    //...
  }

  handleClickBar() {
    //...
  }

  handleClickBaz() {
    //...
  }

  //...

  render() {
    <div className="big-component">
      <div className="header">
        <div className="header-content-1" />
        // ...
        <div className="header-content-99" />
      </div>
      <div className="body">
        <div className="body-content-1" />
        // ...
        <div className="body-content-99" />
      </div>
      <div className="footer">
        <div className="footer-content-1" />
        // ...
        <div className="footer-content-99" />
      </div>
    </div>
  }
}

Good

// Header, Body, Footerを別コンポーネントに分割
class BigComponent extends React.Component {
  render() {
    <div className="big-component">
      <Header />
      <Body />
      <Footer />
    </div>
  }
}

内部要素の抽象化

あるコンポーネントをラップするようなコンポーネントは内部要素を props.children として扱い、責務を分解します。これにより Props のバケツリレーも減らすことができます。

Bad

class ParentComponent extends React.Component {
  return() {
    <div className="parent">
      <ChildComponent1 title={this.props.title} value={this.props.value} />
      <ChildComponent2 title={this.props.title} value={this.props.value} />
    </div>
  }
}

class ChildComponent1 extends React.Component {
  return() {
    <div className="child">
      <h3>{this.props.title}</h3>
      <GrandChildComponent type="foo" value={this.props.value} />
    </div>
  }
}

class ChildComponent2 extends React.Component {
  return() {
    <div className="child">
      <h3>{this.props.title}</h3>
      <GrandChildComponent type="bar" value={this.props.value} />
    </div>
  }
}

Good

class ParentComponent extends React.Component {
  return() {
    <div className="parent">
      <ChildComponent title={this.props.title}>
        <GrandChildComponent type="foo" value={this.props.value} />
      </ChildComponent>
      <ChildComponent title={this.props.title}>
        <GrandChildComponent type="bar" value={this.props.value} />
      </ChildComponent>
    </div>
  }
}

class ChildComponent extends React.Component {
  return() {
    <div className="child">
      <h3>{this.props.title}</h3>
      {this.props.children}
    </div>
  }
}

Stateless Functional Componentを使う

State を持たないコンポーネントは Stateless Functional Component(SFC) として関数の形で書くことができます。State を持たない純粋関数とすることでそれだけ副作用を減らせるので、プレゼンテーショナルなコンポーネントはなるべく Stateless にすることをおすすめします。SFC の形で書くことでチーム開発においても Stateless であるという意図が伝わりやすくなるでしょう。

Bad

class Foo extends React.Component {
  render() {
    return (
      <div className="foo">{this.props.value}</div>
    );
  }
}

Good

function Foo(props) {
  return (
    <div className="foo">{props.value}</div>
  );
}

継承ではなく Higher Order Component を使う

あるコンポーネントに共通の振る舞いを持たせたい時、継承ではなく Higher Order Component(HoC) パターンを使う方が依存関係などの見通しが良くなります。HoCは他のコンポーネントをラップして機能を追加したコンポーネントです。

Bad

class InputWithHandler extends React.Component {
  constructor() {
    super(...arguments);
    this.state = {
      value: null
    };
  }

  handleChange(e) {
    this.setState({ value: e.target.value });
  }

  render() {
    return (
      <input
        value={this.state.value}
        onChange={this.handleChange.bind(this)}
      />
    );
  }
}

class SelectWithHandler extends InputWithHandler {
  render() {
    return (
      <select
        value={this.state.value}
        onChange={this.handleChange.bind(this)}
      >
        <option value="foo">foo</option>
        <option value="bar">bar</option>
        <option value="baz">baz</option>
      </select>
    );
  }
}

Good

上記の継承を HoC を返すファクトリ関数で置き換えると以下のようになります。

function handlerHoC(BaseComponent) {
  return class extends React.Component {
    constructor() {
      super(...arguments);
      this.state = {
        value: null
      };
    }

    handleChange(e) {
      this.setState({ value: e.target.value });
    }

    render() {
      return (
        <BaseComponent
          value={this.state.value}
          handleChange={this.handleChange.bind(this)}
        />
      );
    }
  }
}

const InputWithHandler = handlerHoC((props) => {
  return (
    <input
      value={props.value}
      onChange={props.handleChange}
    />
  );
});

const SelectWithHandler = handlerHoC((props) => {
  return (
    <select
      value={props.value}
      onChange={props.handleChange}
    >
      <option value="foo">foo</option>
      <option value="bar">bar</option>
      <option value="baz">baz</option>
    </select>
  );
});

recompose

recompose はHoCを定義するための関数群で、これを使用することでHoCをより簡潔に定義することができます。

const enhance = compose(
  withState('value', 'setValue', null),
  withHandlers({
    handleChange({ setValue }) {
      return (e => setValue(e.target.value));
    }
  })
);

const InputWithHandler = enhance(props => {
  return (
    <input
      value={props.value}
      onChange={props.handleChange}
    />
  );
});

パフォーマンスチューニング

実際のところ ReactComponent そのもののパフォーマンスイシューに遭遇した機会はそこまでありませんが、例えば無限スクロールする ListView や、スプレッドシートのようなセルベースの入力インターフェースの実装といったケースでは、VirtualDOM といえどその差分計算時のコストが原因でパフォーマンスが悪化することがありました。推測するな、計測せよということで、まずは react-addons-perf2 を使ってパフォーマンスの計測を行います。

import * as React from 'react';
import Perf from 'react-addons-perf';

class ParentComponent extends React.Component {
  constructor() {
    super(...arguments);
    this.state = {
      label: 'button'
    };
  }

  onClick() {
    Perf.start();
    this.setState({ label: 'clicked' }, () => {
      Perf.stop();
      Perf.printWasted(Perf.getLastMeasurements());
    });
  }

  render(): any {
    return (
      <ChildComponent
        label={this.state.label}
        onClick={this.onClick.bind(this)}
      />
    );
  }
}

function ChildComponent(props) {
  return (
    <button onClick={props.onClick}>
      {props.label}
    </button>
  );
}

クリックすると一度だけラベルを変更するボタンです。2回目のクリック以降は再レンダリングの必要はなさそうですが、react-addons-perf を使用したプロファイリング結果を見ると2回目以降のクリック時にも ChildComponent まで VirtualDOM のレンダリングが走っていることがわかります。

f:id:t930:20171219183259p:plain

そこで、React のライフサイクルメソッドである shouldComponentUpdate で現在の Props と新しく受け取る Props を比較して再レンダリングが必要か否かを判定するようにします。

compose(
  lifecycle({
    shouldComponentUpdate(nextProps) {
      return this.props.label !== nextProps.label;
    }
  })
)((props) => {
  return (
    <button onClick={props.onClick}>
      {props.label}
    </button>
  );
});

これで2回目以降のクリックイベント発火時に ChildComponent の renderer が走ることはなくなりました。

f:id:t930:20171219183310p:plain

しかし、この形式では Props 毎に比較する必要がある上、列挙している Props に漏れがあった場合再描画されるべき時にされないといったバグを生み出してしまうリスクが生じます。 React v15 から導入された React.PureComponent では shouldCOmponentUpdate をオーバーライドして自動的に Props を比較してくれるので、そちらを使うようにします。

// FYI: recomposeを使用しない場合はReact.PureComponentを継承します
pure((props) => {
  return (
    <button onClick={props.onClick}>
      {props.label}
    </button>
  );
});

PureComponent では shallowEqual で Props の比較を行うため、ネストした Props を扱う場合は差分を正確には検知できません。lodash の deepEqual や Immutable.js などを使って比較することはできますが、あまり複雑になってくると比較そのもののコストがレンダリングのコストを上回る・・・といった事態になりかねないので、その点は天秤にかけながら実装していくのが良いでしょう。

Jestでスナップショットを取る

React と同じく facebook が公開しているテストフレームワーク Jest では通常のアサーションに加えてコンポーネントのレンダリング結果をスナップショットとして保存し次回実行時に比較することができます。

import * as React from 'react';
import renderer from 'react-test-renderer';

test('snapshot', () => {
  const component = renderer.create(
    <div>foo</div>
  );
  const tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});
$ yarn jest

 PASS  sample.test.js
  ✓ snapshot (15ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 passed, 1 total
Time:        1.328s, estimated 3s

一度実行し、スナップショットが作成されました。元のコンポーネントに意図的に変更を加えた上で再度実行してみます。

import * as React from 'react';
import renderer from 'react-test-renderer';

test('snapshot', () => {
  const component = renderer.create(
    <div>bar</div>
  );
  const tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});
$ yarn jest

 FAIL  sample.test.js
  ✕ snapshot (18ms)

  ● snapshot

    expect(value).toMatchSnapshot()
    
    Received value does not match stored snapshot 1.
    
    - Snapshot
    + Received
    
      <div>
    -   foo
    +   bar
      </div>
      
      at Object.<anonymous> (sample.test.js:11:16)
      at process._tickCallback (internal/process/next_tick.js:103:7)

 › 1 snapshot test failed.

Snapshot Summary
 › 1 snapshot test failed in 1 test suite. Inspect your code changes or run with `yarn run jest -- -u` to update them.

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   1 failed, 1 total
Time:        4.245s

前回実行時のスナップショットと比較し、diff が生じていることが検知されました。これにより意図しないレンダリング結果の変更を防ぐことができます。3

Storybookでドキュメントを生成する

チーム開発の中で新しいメンバーがどのコンポーネントを利用すべきかわからなかったり、それにより似たようなコンポーネントが生まれてしまう状況を防ぐために、Storybookを使用してコンポーネントのドキュメントを生成すると良いでしょう。

import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { text, boolean } from '@storybook/addon-knobs';
import DateInput from 'components/input/date_input';

storiesOf('components/input/date_input', module)
.add('with date range', () => {
  return (
    <DateInput
      value={text('value', '2018-01-01')}
      onChange={action('change')}
      minDate={text('minDate', '2017-12-15')}
      maxDate={text('maxDate', '2018-01-15')}
      disabled={boolean('disabled', false)}
    />
  );
})

f:id:t930:20171219183336p:plain

Storybookを使うことで、コンポーネントのユースケース別の解説や、Props変更時の振る舞いについて確認することができます。

最後に

実際の開発ではここに flux によるデータフロー、flowtype による型付けといった要素が加わってきますが、今回はReactComponentそのものにフォーカスして、普段設計やリファクタリング時に気をつけていることなどを紹介させていただきました。少しでも参考になれば幸いです。

明日は社内勉強会を主催されている futoase さんです!お楽しみに〜


  1. 本記事で使用しているReactのバージョンはv15.6.2となります。

  2. React v16以降は非対応となり、ブラウザのデベロッパーツールで計測することが推奨されています。

  3. 意図的に変更がある場合は --updateSnapshot でスナップショット自体を更新することができます。

運用中のサービス間の堅牢性をあげるために、Pact導入を試みた話

この記事は、freee Developers Advent Calendarの18日目です。

はじめまして! freee株式会社のエンジニアの id:Maco_Tasu です。最近の趣味はゲーム実況をYoutubeでみることです。今日は私が所属しているチームで行ったHackweekという取り組みの中で、運用中のサービス間における堅牢性向上を試みた話について書きたいと思います。

Hackweekとは

Hackweekとは私が所属するチームで11月に行った取り組みの一つで、各エンジニアがそれぞれ課題に感じていることに対して各自のアプローチで一週間それだけに集中して行う取り組みです。

私が取り組んだこと

freeeにはマイクロサービスが存在します。いくつかのマイクロサービスでは通信を容易に行えるようにするための専用のクライアントgemがあります。クライアントのテストコードでは、マイクロサービスとの通信箇所の処理はwebmockでマイクロサービスから返ってくるであろう値を返すようにstubしています。このような方法のテストの場合、

  • マイクロサービス側のみ修正して、クライアント側でstubしている箇所の修正漏れなど起こっていても気づきにくい
  • マイクロサービス側のレスポンスの値を変更すると、マイクロサービス側のテスト、クライアント側のstubしている値の二箇所を修正する必要がある

以上のような事が起こりえます。ここで一番問題なのは、マイクロサービス側の修正を行った際に、クライアント側の修正漏れが起こり得ることです。現在は念入りな動作確認やQAさんのチェックを通しているので、そのタイミングで問題には気付けるのですが、ここもより早いタイミングで機械的に検知できるようにすることでサービス間の堅牢性につながるのではないと考えていました。そこで思いついたこととしては、クライアントがリクエストを正しく処理できることを正だとみて、それにマイクロサービスが期待するレスポンスを返せるかという部分をテストで検知できたらこの問題解消されるのでは?と考えていました。これを実現するためのいい感じのツールがないかと色々なテスト方法を検索していて調べたところ、Cookpadさんが執筆されていた実践 Pact:マイクロサービス時代のテストツールという記事をみつけて「これだ!」となりPactの導入を試みてみました。

Pact とは

Consumer-Driven Contract testing を実現するためのツールです。クライアント駆動でテストを作成し、クライアントが期待しているリクエストを実際にサーバーが返せるかの検証をすることができます。実際に使う際には、大きく以下のような手順を踏むことになるかと思います。

  1. consumer(クライアント) にPactのgemを導入して、rspecなどで作ったAPI呼び出し処理に対するテストケースを作成します
  2. 1を実行した際のリクエストとレスポンスがPact fileに記録され吐き出されます。
  3. privider(マイクロサービス) にPact fileのテストを実行するのに必要な初期データを作成します。
  4. 初期データが作成できたらprivider側でbundle exec rake pact:verifyを実行します
  5. この際consumerが期待するレスポンスをprividerが返せるか pact fileにそって検証されます

参照: https://github.com/pact-foundation/pact-ruby

こうやってみると意外にすぐできそうです。今回はこれを運用中のサービスに導入を試みました。

運用中のサービスへの導入

運用中のサービスに導入しようとした場合、クライアント側はwebmockを既に使っていたら少しの設定の追加とwebmockからの置き換えの対応が必要です。

クライアント側

1. まずPactがテスト時にリクエストが受け付けるためのmock serverが起動できるように設定を追加します

spec_helper以下のような設定を追加します

require 'pact/consumer/rspec'

Pact.service_consumer "client" do
  has_pact_with "microservice" do 
    mock_service :microservice do
      port 3001 # 適当なポート
    end
  end
end
2. 次にwebmockを使用している箇所はPactのmock serverを参照するように以下のように変更します

例えば以下のようなwebmockの設定があったら次のような感じで書き換えます。

before: mock_server

stub_request(method, "#{host}#{path}?#{string_query}").
      to_return(status: status, body: body.to_json)

after: pact

string_query = "#{params.to_query}"
description = "#{self.class.it.full_description}"
escape_path = URI.escape(path)
mock_service.given(description).
      upon_receiving(description).
      with(method: method, path: escape_path, query: string_query).
      will_respond_with(
        status: status,
        headers: {
          'Content-Type' => Pact.term(generate: 'application/json', matcher: %r{application/json}),
        },
        body: body
      )

webmockからPactに置き換えた場合でも、Pactがテスト中にmockしたAPIはコールされたかなど検証してくれます。 ここで記述されているupon_receiving(description)の箇所は、マイクロサービス側でテストを実行する時にクライアント側のどのテストと結びついているかを表す識別子的な役割を果たします。そのためここはユニークな値でなければいけないのですが、私の場合、"#{self.class.it.full_description}"を渡すここでとりあえずうまくいっています。

3. Pactを適用させたいテストケースに以下を追加
describe 'test', pact: true do
  ...
end

例のようにpact: trueの書いてあるテストケースのみPactが有効になり、テスト実行時にファイルに書き出されるようになります。この設定があるおかけで運用中のサービスのテストケースに少しずつテストケースを反映することができ、導入のハードルが下がりました。ここで気をつけないといけないのは、一つのテストファイルでwebmockを使いつつPactもつかいたいケースで、Pactを適用させたテストケース実行時にはwebmockを無効にしないとwebmock側で、「そんなリクエストはモックしてないのでしらないぞ!」って怒られてしまいます。そこで柔軟に対応できるように私はspec_helperは次のような設定に変更しました。

RSpec.configure do |config|
  require 'webmock/rspec'
  include WebMock::API

 config.before :suite  do
    WebMock.enable!
  end

 config.before :all, pact: true do
    WebMock.disable!
  end

 config.after :all, pact: true do
    WebMock.enable!
  end
end


def mock_request(method: :get, host: '127.0.0.1:9292', path: '/', params: {}, body: {}, status: 200)
  if self.class.metadata[:pact]
    # pactを使ったmock
  else
    # webmockを使ったmock
  end
end

以上のような設定を追加することで、一つのファイルでwebmockもPactも共存できるようになります。

マイクロサービス側

こちらはクライアント側で作成されたテストケースに対しての初期データ設定のみ対応が必要です。基本的にはControllerのテストが正しく書かれていたらそこで作成されているテストデータを、Pactから使われる箇所へ移植する作業になります。導入にあたる手順は以下になります。

1. クライアント側が生成したファイルをPactが読み取れるように設定を追加します

ruby Pact.service_provider 'microservice' do honours_pact_with 'client' do pact_uri 'jsonファイルが置かれている場所を指定' # 手元で動かすには相対パスでクライアントが吐き出したファイルを指定する感じです。 end end

2. ファイルに書かれたテストケースと対になるテストデータを追加します

基本的にはよくあるFactoryGirl(FactoryBot)、DatabaseCleanerを使ったテストデータの作成方法と同じです。

# FactoryGirlの設定
Pact.configure do | config |
  config.include FactoryGirl::Syntax::Methods
end

# 各テスト時にデータがクリアされるようにする
Pact.set_up do
  DatabaseCleaner.strategy = :transaction
  DatabaseCleaner.start
end

Pact.tear_down do
  DatabaseCleaner.clean
end


Pact.provider_states_for 'client' do
  provider_state 'クライアントで設定しているテストケース名' do # 先述した upon_receiving(description) の名前
    set_up do
       # テストデータセットアップ
    end

    tear_down do
       # セットアップデータ破棄
    end
  end

  # ... 以下テストが続く
end

以上で準備完了です。あとはマイクロサービス側で

bundle exec rake pact:verify

を実行するとクライアント側が吐き出したファイルに沿ってマイクロサービス側でリクエストを受けて、期待するレスポンスを返せるか検証されます。今回は手元で確認するところまでしか導入できてませんが、これをCIなりにのせて自動化していくのが次のスッテプかなというところです。

導入してみて感想

初見のツールだったので仕様の把握に少し時間はかかったものの、一度仕組みさえ整えてしまえばPactを意識しないで今まで通りテストがかくことができました。導入して他に気づいたことや感想は以下になります。

  • [気づき] 現状stubしている箇所で、requestまたはresponseも追従できないものがいくつか見つけることができた
  • [気づき] 現状追従できていない箇所があるものので問題になっていないのは、入念な動作確認やQAの皆様のチェックのおかげが大きいことに改めて気づけた
  • [感想] 実際にクライアント・マイクロサービス間のリクエストを機械的に検証できるのはすごく安心感ある
  • [感想] テストケースごとにPactを使うことができるので、徐々に導入していけて運用中のサービスからでも使いやすかった
  • [感想] Pactを使ったクライアント側のテストがしっかりしていたら、マイクロサービス側のControllerのテストで意味が重複してくるのでテスト自体なくてもいいのではないかと感じた

以上のような感想をいだきました。

最後に

いかがでしたでしょうか。今回はクライアント・マイクロサービス間の堅牢性向上のために、Hackweekを通して運用中のサービスにPactを導入する時に実際にどうやったかという点と、気をつけないといけない箇所について触れました。少しでも同様の問題でお悩みの方への参考になりましたら幸いです。

明日19日目は、freeeのエンジニアで多彩な趣味をお持ちなtakumiさんです!お楽しみに!

PM(Product Manager)って何やってるのか具体的な案件を見ながら説明してみる

この記事は freee Developers Advent Calendar の17日目です。

自己紹介

freee 株式会社で、PM(Product Manager)をやっているfuji_tipです。

freeeに入ってから4年で、マーケティング/事業開発 —> データ分析 —> 事業戦略 —> PM という変遷で、社内ジョブホッパーです。フルスタック社員と自称しています。

趣味は飲酒です。

PMってなにやってるの

社内外から、PMって何やってるかわからない、どういう能力があればPMになれるのかわからないなどの声をもらうことが多いので、具体的な案件のリリースまでのプロセスを振り返りながら、PMの仕事について理解いただければと思います。

ある機能を作る!とか既存の機能改善をする!となったときの大体の流れを簡単に下記の通り説明します。

  • 課題選定・ゴール設定
    • それが本当に課題なのか?課題だとしたら、どのような状態になるのが理想か?
  • ソリューション検討・影響調査
    • 課題に対してどのようなソリューションが最適か?それはお客様にどのような影響をあたえるか?他の機能に影響はないか?
  • 開発
    • ソリューションは実現可能か?PMが作った仕様に抜け漏れはないか?
  • リリース・振り返り
    • 設定した課題・ゴールはあっていたか?お客様からのフィードバックはないか?

では、次からは具体的な改善案件を扱いながら、どういうことをやっているのか説明したいと思います。

具体的案件:取引一覧のパフォーマンス改善

課題選定・ゴール設定

会計 freeeは会計・経理ソフトであるため、月末月初に利用されるお客様が多く、かつ、SaaSであるがゆえに、多くのお客様に利用されると負荷が月末月初に集中します。

また、創業当初は個人事業主の方にお使いいただくようなサービスだったのですが、事業の進捗により、個人事業主の方から数百名規模の企業にいたるまで、様々な規模の多くのお客様からお使いいただけるサービスに進化してきました。

その為、月末月初にかかるサービスへの負荷が無視できないレベルになってきており、その中でもよく使われる”取引一覧”画面の負荷が高いということがわかりました。

f:id:fuji_tip:20171215144628p:plain こちらのデータはデモアカウントのものです。

そこで、freeeの取引検索機能は、検索項目や条件を変更するたびに即座に検索が実行される仕組みとなっているのですが、取引一覧の中でも、この検索機能の負荷が高いのではないかという仮説が浮かび上がりました。

検索ボタンを設置し、ボタンを押したときにだけ検索を実行しようというアイデアもあったのですが、freee側都合でお客様への価値を下げるのはいかがなものかと思い、一度下記のように、"負荷"の原因を分解し、実際に起こっている事象を整理してみました。

負荷の原因 事象
取引一覧での、一度の検索リクエストの負荷が高い(一度にたくさんデータを持ってきている) 取引一覧に遷移したときに表示される取引は、なんの条件もなく全件表示される(厳密には全件なめる)ようになっている
取引一覧での、リクエスト回数が多い(検索実行される回数が多い) 検索条件や項目を変更するたびに検索が実行される

こうすることで、課題の本質が明らかになったので、ソリューションを考えやすくなりました。

ここで、今回の改善のゴールは下記のようなものになります。

ゴール

  • (freee都合の変更なので)お客様の体験がなるべく変わらない
  • 取引一覧の負荷が下がる

次からソリューションを検討していきます。

ソリューション検討・影響調査

「一度の検索リクエストの負荷が高い」という課題に対しては、取引一覧に遷移時の初期表示の件数を減らすことが必要なので、初期の表示条件に取引日付を設定することとしました。

これは、現行の取引一覧が取引日付順で並んでいるため、条件を取引日付以外にすると、初期表示される取引が現状と変わってしまうためです。変わってしまうと「お客様の体験を変えない」というゴールが守れなくなってしまいます。

その上で、「お客様の体験が変わらない」を 取引一覧の1ページ目に表示される取引の数が変わらない と定義し、実際に1ページ目に表示される取引件数をプロダクトのデータを使って調べてみました。

取引一覧の1ページあたりの件数は20〜500件までと変更できるので、最大の500件を表示件数とした時に体験が変わらない割合を算出しました。

この時、期間は現在日付から3ヶ月前、6ヶ月前、9ヶ月前、12ヶ月前、会計期間の初めからと設定して算出しています。

※一部数字を伏せています。縦軸の最大値が100%じゃない場合があります。

お客様の体験が変わらない割合

f:id:fuji_tip:20171214224712p:plain

抽出される取引の件数割合(総数に対する割合)

f:id:fuji_tip:20171214224906p:plain

なるべく多くのお客様の体験が変わらず、かつ、負荷が十分に下がる点を条件とし、

  • 現在日付から12ヶ月前もしくは現在の会計期間の開始日のどちらか古い日付以降とする

という条件を、取引一覧の初期表示時の条件に設定しました。

「リクエスト回数が多い」という課題に関しては、検索条件が変わらない項目変更時は、検索の実行を走らせないというソリューションにしました。(こちらはまだ未実装でリリースされていません)

また、考えたこれらのソリューションをドキュメントにまとめて共有し、社内の様々なチームに確認をしてもらい、問題がなさそうであることを確認します。 f:id:fuji_tip:20171214223510p:plain

開発

開発自体はエンジニアにやってもらうのですが、なぜやるのか?を重点的に説明します。 背景を理解してもらうことによって、PM側が考えた仕様に抜け漏れはないか、他に影響はないか?など、より広範に渡ってお客様に影響がないかどうかを考えながら作ってもらえます。

作ってもらった機能を検証環境で実際に触り、挙動に問題がないかなども確認したりします。

リリース

プロダクトにリリースする前には、お客様への告知を行います。

具体的には、ホーム画面へのお知らせの掲載や、お客様へのメール送信などを行います。お知らせの掲載は管理画面から自身の手でおこない、メール送信は担当チームに伝えてやってもらいます。

f:id:fuji_tip:20171214223657p:plain

いよいよリリース日、お客様に受け入れられるだろうか、バグは起きないかなど、様々な想いを頭にめぐらせ、そわそわしながら反応を待ちます。

リリースから数週間が経ちますが、お客様からのネガティブなフィードバックはなく(ポジティブなフィードバックもないのですが)、かつ、負荷を大幅に低減させることができました。

f:id:fuji_tip:20171214223827p:plain

ゴールに、お客様の体験が変わらない&負荷が下がることを設定していたので、成功といえます。 (体験が変わっていないからこそ、フィードバックがないととらえました。)

総括

ここで紹介したのは比較的地味なプロダクトの改善でしたが、新機能の開発なども大枠同じようなプロセスで行っていきます。

今回は、ゴールをお客様の体験を変えないことに置きましたが、基本的な機能改善や新機能開発などは体験が変わることが前提となります。そのため、新しい機能をどのようにお客様に理解してもらうか?使ってもらえるか?も重要なポイントになってきます。このあたりはUXデザイナーと一緒になって考えていったりします。 (今回の改善ではUIの変更は伴わなかったため、デザイナーとの協同はあまりありませんでした)

その他にも、事業計画を達成するためにはどのようなプロダクトがどういう時間軸で存在すべきか?などのような、より抽象的な課題を考えるPMもいたりします。

また、これはfreee 株式会社におけるPM業の一例です。 PMという職種自体発展途上な部分がありますので、業種やビジネス規模、事業フェーズに応じて役割が違うことも多々あると思います。

PMのお仕事、ご理解いただけたでしょうか?

課題発見・設定力、情報収集・分析能力、コミュニケーション能力など様々なスキルセットが求められる仕事で、意思決定を迫られる機会が非常に多いためプレッシャーもありますが、お客様に価値を届ける源泉になる職種で、非常にやりがいがあります。

明日は、freeeのセキュリティを堅守する期待の若手エンジニア、macotasuさんです〜。お楽しみに!