WESEEK Tech Blog

WESEEK のエンジニアブログです

Go 言語での開発を試してみる 〜調べる編〜

こちらは 「Go言語での開発を試してみる 〜調べる編〜」からの転載です。

2018 年 8 月時点での Go 言語関連の調査結果を紹介しています。


こんにちは。haruhikonyan です。 自分 Go 言語というものを実はこれまで触ったことが無かったのでちょいと触ってみることにしました。 とはいえ何がやりたいとかそういうことは無いのでとりあえず Hello World と開発のための道具としてはどういうものがあるかを調べてみようと思います。

Go 言語のインストール

まずはインストールから。 調べるにもとりあえず手元で go コマンド自体が動かないとつらいものがあると思うのでインストールします。

windows

  1. インストーラダウンロード
  2. インストール
  3. バージョン確認

     > go version
     > go version go1.10.3 windows/amd64
    

無事インストールされて PATH も自動的に通してくれています!

mac

  1. homebrew でインストール

     $ brew install go
    
    • 自分は普段から homebrew を使っているのでインストーラは使わずに homebrew でインストールします。
  2. バージョン確認

     $ go version
     go version go1.10.3 darwin/amd64
    

こちらも無事インストール完了です。

Hello World

書いてみる

とりあえず公式のトップにある下記コードを拝借し、保存。

package main

import "fmt"

func main() {
  fmt.Println("Hello, 世界")
}
$ go run hello.go
Hello, 世界

簡単ですね!

コンパイル

Go 言語で書いたコードはデフォルトでクロスコンパイルに対応しているということで早速試してみます。

Windows

windows環境で試しているのでそのままビルド

> go build hello.go

なんとexeが吐き出されます! それをcmd上で実行してみると?

> hello.exe
hello, 世界

ちゃんと出力されました!

Mac

次は mac 用に出力してみることにします。

$ SET GOOS=darwin
$ SET GOARCH=amd64

$ echo %GOOS%
darwin
$ echo %GOARCH%
amd64

上記のように環境変数をセットしてあげます。 すると、hello というバイナリファイルが出力されました。 そいつを手元の mac に送ってターミナルで実行してみると?

$ ./hello
hello, 世界

どんな環境でも同じコードで動く!楽しい!

本格的に開発するために必要なうんちく

この章は Go 言語あまり関係ないんで飛ばしてくれて構いません。 Go 言語開発を始める上で他言語で開発を始める上で気にしていることを述べています。

フレームワークの有無

開発を行う上で言語の選定と同じ、それ以上に重要なこととしてフレームワークの選定があると思います。 何を作るにしても大体は先人が同じようなことを行なっていることがほとんどで、そのライブラリであったり、フレームワークを選定し、優れたフレームワークのある言語で開発を行うという選択が開発を成功させることであったり、工期を短くする秘訣であったりします。

例えば Ruby には Ruby on Rails という優れた Web フレームワークがあったからこそ莫大な人気が出たと言っても過言ではありません。 他にも Java の Spring や PHP の Laravel、 PythonDjango といったフレームワークが人気が高く幅広く使われていると言えるでしょう。

また、フレームワークを採用することでチーム開発をする上でのコーディング規約などが自然と揃い、読みやすくなるという利点もあると言えるでしょう。 車輪の再開発を悠長にやっている暇は無いのです。使えるものを使いましょう。

開発エディタ候補

開発環境においても VSCodeAtom のようなテキストエディタ拡張機能を追加して機能を充実させていくようなタイプのエディタもあれば、Java でいう Ecripce や IntelliJ のようなすでにデバッガ機能などが充実した IDE (統合開発環境)という大きな二つの選択に分かれると言えます。

前者のテキストエディタがベースのものではまず軽いといったメリットがありますが、機能を増やすには拡張機能を探して自分で入れる必要があったり、全ての要件を満たそうとすると自分で色々調べたりしなければならないのが欠点とも言えると思います。また、全てを詰め込もうとするとエディタ自体も重くなってしまうとうことも十分考えられます。 後者の IDE では専用ということもあり、機能は一通り揃っているが、エディタ自体が重いということもよくあることです。

エディタはよく Vim vs Emacs というような宗教戦争などと揶揄されるようにエンジニア個人の好みも大きく分かれるところです。開発効率にも大きく影響する部分だと思うのでベストな選択をしたいところです。

パッケージ管理

開発を行う上でライブラリやパッケージの依存管理はほぼ必須と言ってもいいでしょう。Node.js には npm や yarn といったものがあったり、Ruby では RubyGems (gem) であったり、他にもさきほど Mac で Go をインストールするために使った Homebrew もパッケージ管理システムです。

これらにパッケージ管理を任せることで、だれがアプリをビルドしても必ず同じパッケージ構成にできるというのも大きな利点の一つです。 開発をする上で依存関係を人間が細かく気にすることなく進める上では欠かせない存在であると言えます。

Go で本格的に開発するための道具候補

フレームワーク

Web フレームワークばかりになってしまいましたが、フレームワークの紹介です。

Gin

beego

Echo

iris

Revel

Martini

Gorilla

開発エディタ候補

公式ページによると4つのエディタが紹介されていました。

vim

Visual Studio Code

GoLand

Atom

パッケージ管理

dep

glide

vgo (Versioned Go)

使わない

  • パッケージ管理しないといけないような粒度の成果物つくるのは Go じゃなくていい説という話も。。。

次回予告

次回は後編ということで実際にこれらの調べた要素を組み合わせて簡単な何かを作ってみたいと思います。

Angular Tips その3

こちらは 「SEROKUを支える技術 Angular Tips編 その3」からの転載です。

記事の最後に関連記事を掲載しています。よろしければご参考にどうぞ。




さて、今回は前回予告したとおり ng-content を使った、テンプレートに対して外部から子要素を突っ込むような処理を書いてみようかと思います。

Angular に置いてあまりサンプル記事がない印象ですが、使い方さえ覚えておけばきっと役に立つはずです!

Tips 4 :ng-content を使った子要素への注入

ng-content とは、作成したコンポーネントに対して外部から要素を注入するときに使うタグです。

ng-content を使ったシンプルな例

まずは一番簡単な例を、コードベースで書いていきたいと思います。

selector: ‘foo’ とする Foo コンポーネントを定義

<div>
  <h1>header要素</h1>
  <!-- 以下ng-contentを配置し、外部から注入するための要素を宣言 -->
  <ng-content></ng-content>
</div>

外部のコンポーネントで foo を呼び出し、さらに要素を注入

    <foo>
      <div>test</div>
      <span>test2</span>
    </foo>

上記の foo コンポーネントを呼び出した結果の html

    <div>
      <h1>header要素</h1>
      <!-- angular によって展開され、foo タグに書いたタグが全て ng-content に展開される -->
      <div>test</div>
      <span>test2</span>
    </div>

foo タグ内部に書いた要素が、まるまる ng-content と置き換わりますね。 上記の簡単な例を実装する上ではうまくいきましたが、では複数の要素を別々の場所に注入したい場合はどうすればよいでしょう?

ng-content を使い複数の要素を注入する例

ng-content では select アトリビュートを使い、注入するコンテンツを指定することができます。 以下のコードで複数の要素を注入したいと思います。

selector: ‘foo’ とする Foo コンポーネントを定義

<div>
  <!-- ng-contentを宣言(selectでh1を指定) -->
  <ng-content select="h1"></ng-content>
  <!-- ng-contentを宣言(selectで.headerを指定) -->
  <ng-content select=".header"></ng-content>
  <!-- 以下ng-contentを宣言(select無し) -->
  <ng-content></ng-content>
</div>

外部のコンポーネントで foo を呼び出し、さらに要素を注入

    <foo>
      <h1>test</h1>
      <div class="header">test2</div>
      <div>test3</div>
      <span>test4</span>
    </foo>

上記の foo コンポーネントを呼び出した結果の html

    <div>
      <!-- select で h1 を指定しているので h1 タグの要素が展開される -->
      <h1>test</h1>
      <!-- select で .header を指定しているので header クラスを指定した要素が展開される -->
      <div class="header">test2</div>
      <!-- 上記指定に当てはまらないタグが全て ng-content に展開される -->
      <div>test3</div>
      <span>test4</span>
    </div>

foo タグ内部に書いた要素の中で、Foo コンポーネントの ng-content の select で指定した箇所にそれぞれ置き換わりますね。

ちなみに、上記の例で言うと h1 タグが複数あった場合、または header クラスが複数あった場合でも、select にマッチする要素分 ng-content は置き換わります。

ng-content を使った例 応用編

上記の例はあくまでただの使い方の例だったので、どういう時に役に立つかをユースケースを交えながら書いていきたいと思います。

一つの例として、コンポーネントを作る際に、あるボタンイベントを親コンポーネントに EventEmitter を使って通知し、実際の処理は親コンポーネントでやらせているケースがあるとします。

selector: ‘foo’ とする Foo コンポーネントを定義

// テンプレート側
<div>
  <button (click)="clickHandler()"></button>
</div>


// コンポーネント側
@Output()
onClicked = new EventEmitter();

clickHandler() {
    onClicked.emit();
}

外部のコンポーネントで foo を呼び出し onClicked に対してイベントハンドラを仕掛けている

// テンプレート側
<foo (onClicked)="clickHandler()" ></foo>

// コンポーネント側
clickHandler() {
    // 実際の処理
}

Foo コンポーネント側の clickHandler で、なにか特別なことをしているのなら別ですが、ただ emit しているだけならば以下のように書いたほうが、Foo コンポーネントはロジックを気にせずにただの View テンプレートとしての役割に専念できるので見通しが良いです。

selector: ‘foo’ とする Foo コンポーネントを定義

// テンプレート側
<div>
  <ng-content select="button"></ng-content>
</div>


// コンポーネント側
処理がなくなる!!

外部のコンポーネントで foo を呼び出し click ハンドラを設定している button タグを要素として注入

// テンプレート側
<foo>
    <button (click)="clickHandler()"></button>
</foo>


// コンポーネント側
clickHandler() {
    // 実際の処理
}

上記は一例に過ぎませんが、同じような状況は結構ある気がします。 これ使ったほうがコードがキレイになりそうというのがもしあれば試してみると良いかもしれません。

まとめ

今回は ng-content に関して書きました。

前回の記事で ngTemplateOutlet にも少し触れましたが、ある特定の要素を置き換えるという場合にも複数のやり方がありますね。(もちろんそれぞれで適切な使い所は異なりますが)

吐き出したいネタが一旦尽きてしまったので、なにか思いついたらまたアップしたいと思います。

それでは皆さんごきげんよう

関連記事

tech.weseek.co.jp

tech.weseek.co.jp

イマドキの JavaScript 開発で使える、リモートデバッグとロガーの Tips (中編)

こちらは 「イマドキの JavaScript 開発で使える、リモートデバッグとロガーの Tips(2018年版-中編)」からの転載です。

SEROKU の開発を例に、弊社で使っているリモートデバッグとロガーの Tips をご紹介します。 当記事は 2018 年と過去の記事ですが、現在でも応用可能な Tips になっています。

記事の最後に関連記事を掲載しています。よろしければご参考にどうぞ。




案件としても OSS 成果物としても、JavaScript を利用するシチュエーションは増え続けています。まだまだ枯れた言語とは言い難い状況で、使われるバージョンも ES5 から ES7 まで進化を続け、新しい文字列リテラルや async/await のような「イマドキの JavaScript の書き方」を紹介する記事は多い中、デバッグはこうあるべきという情報は比較的少ないように思います。

本記事の前編にあたる「イマドキの JavaScript 開発で使える、リモートデバッグとロガーの Tips (前編)」では、システム開発に於けるデバッガ、ロガーの大切さと、他の言語・フレームワークと比べた際の JavaScript 開発環境に於けるビハインドについて説明しました。

本記事ではいよいよ JavaScript の世界での理想的なロガーの具体的な設定方法を紹介します。

理想の世界

まずは前編で掲げたゴールの再掲です。

対象システム

以下の3つのシナリオをカバーすることにします。

  • シナリオA
    • node.js (Express)
  • シナリオB
    • node.js (Express) + webpack/babel によるクライアントビルド
  • シナリオC
    • next.js on Express

やりたいこと

  • 記述側
    • サーバーサイド、クライアントサイド両方で同じログモジュールを利用できる
    • ログレベルを指定したログ出力行の記述ができる
  • 出力側
    • サーバーサイド、クライアントサイド両方で同じログモジュールを利用できる
    • development, production 等の環境用の設定ファイルでデフォルトの挙動を設定できる
      • 環境変数設定により、上記を一時的に変更できる
    • プロダクト全体でのデフォルトログ出力レベルを設定できる
    • ログのネームスペースごとにログ出力レベルを設定できる
    • development, production 等の環境に応じて出力フォーマットを変更できる
      • development 環境ではフォーマットされたログ出力
      • production 環境下では JSON フォーマット出力による高速なログ出力
  • 過去の資産の有効利用
    • 今まで記述された debug ライブラリによる記述を変更することなく、新しいロギングライブラリによる出力を可能にする

利用ライブラリ選定

node.js 環境向けのロガーでは、winstonBunyanlog4jspino あたりが有名処です。

npm の weekly download, Github のスター数で言えば、勢力図は 2018/08 現在以下のようになっています。

npm package License weekly download Github stars
Winston MIT 2,165,220 10,998
log4js Apache-2.0 1,199,467 3,321
Bunyan MIT 372,873 5,238
pino MIT 91,535 2,873

pino (選外)

上記のうち、pino は筆者も実際に Production 環境で利用したことがあり、その処理速度は素晴らしいものがあります。Extreme Mode の発想なども興味深く、設計と API 双方からスピードに対するこだわりがにじみ出ていました。 Github の Issue の開発スタッフのストイックさも特徴的です。

ただ、ログレベルと出力先(出力方法)の設定が他ライブラリと比べて貧弱で、今回の理想の世界を実現する上で足枷となったため、選外としました。

log4js (選外)

log4js は名前からも分かるとおり、log4j を意識したライブラリです。Appender や Layouts など、Java 界隈のエンジニアには概念的に馴染みやすいものと言えるでしょう。

こちらは機能的には問題ないものの、ブラウザ利用向けの周辺ライブラリが見当たらなかったことから選外としました。

Bunyan を採用

残る2つ、Winston と Bunyanは機能的にはどれも申し分ありません。今回は勢力図では3番手(Github stars では2番手)である Bunyan (バニヤンと読むらしいです) を採用することにします。

コラム: Winston と Bunyan

Winston と Bunyan の違いは何でしょうか?
比較記事としてはかなり古いのですが以下が有名です。

Comparing Winston and Bunyan Node.js Logging

こちらの記事からは、ストレージへのログ書き込みを主眼としていた Winston に比べて、Bunyan にはシンプルで魅力的なアイデンティティーを持っていることがわかります。

  • JSON 出力を標準とした作り
  • 書き込み対象として Writable Stream interface を意識した作り

これらはコンテナ時代にマッチしますし、出力先がどこであれストリームとして扱うというのは、よりクリーンな設計思想に見えます。

上記記事は執筆された時から長い時間が経過し、つい先日(2018.06)には Winston 3.0 がリリースされました。強力な Transport は Winston の大きな特徴でしたが、winston-transport という

Stream implementation & legacy Transport wrapper.

が外部モジュールとして作成され、従来の Transports エコシステムをそのまま利用可能なストリーム実装、という形式にコア部分が書き直されたようです。思想的には Bunyan に寄った形になったと見ていいでしょう。ただし、リリースされてまだ間もないため、信頼性という意味では v3 系の採用はもう少し時間をおいた方がいいかもしれません。

準備 ― 依存関係整備

ではここからは実際のコードの紹介です。 まずは package.json から。シナリオA~Cまで全てで必要になるものを全て挙げます。

"dependencies": {
    "browser-bunyan": "^1.3.0",
    "bunyan": "^1.8.12",
    "bunyan-format": "^0.2.1",
    "express-bunyan-logger": "^1.3.3",
    "minimatch": "^3.0.4",
    "module-alias": "^2.0.6",
    "next-bunyan": "^0.0.1",
},
"_moduleAliases": {
    "@services/logger": "lib/service/logger"
},

「"_moduleAliases" ってなんだよ??」と思われる方もいらっしゃるかもしれません。ひとまずおまじない的に書いておいてください。便利な使い方を後述します。

Let's Try! ― シナリオA

シナリオAは、node.js (Express) 環境向けのロガー設定です。

設定用ファイル

利用時のイメージを掴みやすいよう、コンフィグファイルから作っていきましょう。

config/logger/config.dev.js

module.exports = {
  default: 'info',

  //// configure level for name
  // 'myapp:*': 'debug',
  'myapp:utils:third-party': 'error',
};

config/logger/config.prod.js

module.exports = {
  default: 'info',
};

上記2ファイルは、それぞれ開発時の設定ファイル、本番環境用の設定ファイルです。
'${ログネームスペース}': '${ログレベル}' という書式の記述をカンマ区切りで列挙できるような形式です。ログネームスペースはここではコロン区切りの blob となっていますが、実際にはどんな文字列でも構いません。

デフォルトのログレベルは default というキーで指定できます。

そして以下は、今し方作成した設定を NODE_ENV 毎に読みわけるためのファイルです。

config/index.js

function envShortName() {
  switch (process.env.NODE_ENV) {
    case 'production':
      return 'prod';
    default:
      return 'dev';
  }
}

module.exports = {
  logger: require(`./logger/config.${envShortName()}`),
};

サービスモジュール

次に、ロガーインスタンスを作成するようなサービスモジュールを作成します。まずは上で定義したような設定ファイルを読み込む機能は考慮しないコードです。

services/logger/index.js--(WIP)

const bunyan = require('bunyan');   // will be replaced to browser-bunyan on browser by next-bunyan

const isBrowser = typeof window !== 'undefined';
const isProd = process.env.NODE_ENV === 'production';
const isTest = process.env.NODE_ENV === 'test';

let stream = isTest
  ? require('./stream.test')
  : isProd
    ? require('./stream.prod')
    : require('./stream.dev');

// logger store
let loggers = {};


/**
 * determine logger level
 * @param {string} name Logger name
 */
function determineLoggerLevel(name) {
  if (isBrowser && isProd) {
    'error';
  }

  return 'info';
}

module.exports = (name) => {
  // create logger instance if absent
  if (loggers[name] == null) {
    loggers[name] = bunyan.createLogger({
      name,
      stream,
      level: determineLoggerLevel(name),
    });
  }

  return loggers[name];
};

まだこのファイルだけでは動作させることはできません。このファイルから参照するストリーム設定用のファイルが必要です。

ストリーム設定ファイル

まずは開発環境用のファイル。

services/logger/stream.dev.js

const isBrowser = typeof window !== 'undefined';

let stream = undefined;

// browser settings
if (isBrowser) {
  const ConsoleFormattedStream = require('@browser-bunyan/console-formatted-stream').ConsoleFormattedStream;
  stream = new ConsoleFormattedStream();
}
// node settings
else {
  const bunyanFormat = require('bunyan-format');
  stream = bunyanFormat({ outputMode: 'short' });
}

module.exports = stream;

開発環境では、ブラウザ上ではフォーマット済み文字列をブラウザコンソールに、node.js 上では同じくフォーマットされた文字列を実行中のコンソールに出力します。

次に本番環境用。

services/logger/stream.prod.js

const isBrowser = typeof window !== 'undefined';

let stream = undefined;

// browser settings
if (isBrowser) {
  const ConsoleFormattedStream = require('@browser-bunyan/console-formatted-stream').ConsoleFormattedStream;
  stream = new ConsoleFormattedStream();
}
// node settings
else {
  // do nothing
  // output JSON to stdout
}

module.exports = stream;

こちらはブラウザ上の設定は開発環境用と同じですが、node.js 上は stream 変数を undefined のままにしています。Bunyan のデフォルト挙動となり、stdout に JSON 形式で出力されます。

最後にテスト用です。

services/logger/stream.test.js

const bunyanFormat = require('bunyan-format');
const stream = bunyanFormat({ outputMode: 'short' });

module.exports = stream;

ここまでで、require('services/logger')('myapp:mymodule') のように呼び出すことで、myapp:mymodule ネームスペース用のロガーインスタンスを生成することができます。

このままでは最初に作った設定ファイル群が活きないので、services/logger/index.js に手を加えます。

services/logger/index.js

const bunyan = require('bunyan');   // will be replaced to browser-bunyan on browser by next-bunyan
const minimatch = require('minimatch');

const isBrowser = typeof window !== 'undefined';
const isProd = process.env.NODE_ENV === 'production';
const isTest = process.env.NODE_ENV === 'test';

let config = require('../../config').logger;

let stream = isTest
  ? require('./stream.test')
  : isProd
    ? require('./stream.prod')
    : require('./stream.dev');

// logger store
let loggers = {};


// merge configuration from environment variables
const envLevelMap = {
  INFO:   'info',
  DEBUG:  'debug',
  WARN:   'warn',
  TRACE:  'trace',
  ERROR:  'error',
};
Object.keys(envLevelMap).forEach(envName => {   // ['INFO', 'DEBUG', ...].forEach
  const envVars = process.env[envName];         // process.env.DEBUG should have a value like 'growi:routes:page,growi:models.page,...'
  if (envVars != null) {
    const level = envLevelMap[envName];
    envVars.split(',').forEach(ns => {          // ['growi:routes:page', 'growi:models.page', ...].forEach
      config[ns.trim()] = level;
    });
  }
});


/**
 * determine logger level
 * @param {string} name Logger name
 */
function determineLoggerLevel(name) {
  if (isBrowser && isProd) {
    'error';
  }

  let level = config.default;

  // retrieve configured level
  Object.keys(config).some(key => { // breakable forEach
    // test whether 'name' matches to 'key'(blob)
    if (minimatch(name, key)) {
      level = config[key];
      return;                       // break if match
    }
  });

  return level;
}

module.exports = (name) => {
  // create logger instance if absent
  if (loggers[name] == null) {
    loggers[name] = bunyan.createLogger({
      name,
      stream,
      level: determineLoggerLevel(name),
    });
  }

  return loggers[name];
};

かなりごちゃっとしていますが、やっていることは環境変数や設定ファイルの map で定義しているネームスペースごとにログレベルを変更しているだけです。

使ってみよう!

ここまでのコードで、

  • シナリオA
    • node.js (Express)

環境はカバーできたことになります。それでは使ってみましょう。以下のように定義、利用します。

app.js

const logger = require('services/logger')('myapp:app');

(..snip..)

// listen
expressApp.listen(process.env.PORT, err => {
  if (err) {
    throw err;
  }
  logger.info('> Ready on http://localhost:' + process.env.PORT + ' [' + process.env.NODE_ENV + ']');
});

以下のようなショートフォーマットされたログが出力されるはずです。

10:00:00.000Z  INFO myapp:app: > Ready on http://localhost:3000 [development]

require('../../../../services/logger') …あれ、まだ足りない?」

Java と違ってパッケージの概念がない JavaScript では、require や import 時の階層の解決は頭の痛い問題です。

そこで、packages.json に記述した _moduleAliases の出番です。これによってどの階層にあるファイルからでも @services/logger で先ほど作成したロガーモジュールを参照できます。

利用側のコードとしては、どのファイルからでも

const logger = require('@services/logger')('myapp:...');

という書き方で参照可能になります。

環境変数で設定を上書きする

本番ディプロイ後に思わぬトラブルに見舞われ、特定のモジュールのログを吐き出したい、という要求もあることでしょう。以下のように環境変数を設定することで、一時的にログレベルを変更することができます。

  • DEBUG=myapp:app,myapp:routes:*
  • TRACE=myapp:services:passport

キーがログレベルの大文字表現、値がログネームスペースのカンマ区切りとなります。

debug ライブラリ代替

既存の debug ライブラリによるログ出力行が山のようにあるプロジェクトに対して、一つ一つを新しいロガーの記述に書き換えていくのは大変です。

ここでまたpackages.json に記述した _moduleAliases が活躍します。
まずは package.json_moduleAliases に以下を追加しましょう。

package.json

"_moduleAliases": {
    "debug": "lib/services/logger/alias-for-debug"
},

そして alias-for-debug.js を作成します。

services/logger.alias-for-debug.js

/**
 * return 'debug' method of bunyan logger
 *
 * This is supposed to be used as an replacement of "require('debug')"
 *
 * @param {string} name
 */
module.exports = (name) => {
  const bunyanLogger = require('./index')(name);
  return bunyanLogger.debug.bind(bunyanLogger);
};

これで、debug ライブラリを利用して出力されたログは、Bunyan 内でログレベル debug として取り扱われます。具体的には、

const debug = require('debug')('myapp:routes:page');

debug('Debug message!');

のように利用していた部分はそのまま Bunyan で処理されるので、myapp:routes:page ログネームスペースの出力レベルを debug 以上にすることでログが出力されるようになります。

この機能を利用することで、debug ライブラリを利用してログ出力しているサードパーティー製ライブラリのログも、同じ設定ファイル(環境変数)でログの出し分けを設定することができます。

(おまけ) 開発時の Express ログ向け設定

Express のログをどう出したいかは好みになりますが、「ヒューマンリーダブルかどうか」を重視すると、morgan の出力ほど見やすいものはありません。しかもほぼコンフィグレスです。一方Bunyan を使って同等の情報量にしようとすると、設定が煩雑になります。

以下は、低速かつ JSON 出力できない morgan は開発環境のみで利用し、一方で本番環境では express-bunyan-logger を利用して高速に JSON 出力するような設定を行うコードです。

/**
 * initialize logger for Express
 * @param {object} expressApp
 */
initExpressLogger(expressApp) {
  const isProd = process.env.NODE_ENV === 'production';

  // use bunyan
  if (isProd) {
    const expressBunyanLogger = require('express-bunyan-logger');
    const logger = require('@services/logger')('express');
    expressApp.use(expressBunyanLogger({ logger }));
  }
  // use morgan
  else {
    const morgan = require('morgan');
    expressApp.use(morgan('dev'));
  }
}

Let's Try! ― シナリオB

シナリオBは、node.js (Express) + webpack/babel によるクライアントビルド両方で利用できる設定です。

といっても、ここまでに作ったファイルは完全に流用できますので、必要なのは webpack の設定のみです。

webpack.js

(..snip..)

resolve: {
  alias: {
    '@services/logger': 'services/logger',
    // replace bunyan
    'bunyan': 'browser-bunyan',
  }
},

(..snip..)

node.js 向けには package.json_moduleAliases で行っていたことを、webpack でも設定するというだけです。ただし、require('bunyan')require('browser-bunyan') に置き換わることになります。

使ってみよう!

使い方は全く同じです。あえて ES Module での import 風に書くと以下のようになります。

components/Component1.mjs

import loggerFactory from '@services/logger';
const logger = loggerFactory('myapp:components:comp1');

export default class extends React.Component {

  render() {
    logger.debug('test: debug: render() about');
    logger.info('test: info: render() about');

(..snip..)

Let's Try! ― シナリオC

シナリオCは、next.js 向けの設定です。

ここでもシナリオAで作ったファイルはそのまま流用できます。シナリオBで行った webpack 用の設定を、next.js 専用の設定ファイルの書式で書き直すだけです。

next.config.js

const path = require('path');
const withBunyan = require('next-bunyan');

module.exports = withBunyan({
  webpack: (config, { dev }) => {

    (..snip..)

    // resolve
    config.resolve.alias = Object.assign({
      '@services/logger': path.resolve(__dirname, './services/logger'),
    }, config.resolve.alias || {});

    // Important: return the modified config
    return config;
  },

(..snip..)

require('bunyan') => require('browser-bunyan') への置き換えは、next-bunyan がやってくれます。

使ってみよう!

シナリオBと全く同じですので割愛します。

まとめ

中編ではロギングに関して実際のコードを紹介し、node.js, webpack/babel, そして next.js という3つのシナリオ、3つの環境で理想的なロギングを行うことができるようになりました。柔軟なログネームスペース、ログレベルの設定や運用時の一時的な変更だけでなく、これまで debug ライブラリを使っていたコードベースに対しても二重管理することなく美しいログ出力ができるシステムを実現可能です。

手前味噌ですがこれらのコードは移植性も高いので、全てのプロジェクトにファイルをコピーして持っていくだけで同じ効能を得ることができます。チーム内のロギングに関する学習コスト節約にもなるのではないでしょうか。

次回、後編では、デバッガ利用の「理想の世界」を実現する具体的なコードを紹介していきます。

関連記事

tech.weseek.co.jp

Kubernetes 時代の CI/CD「Jenkins X」とは? 〜前編〜

こちらは 「Kubernetes 時代の CI/CD「Jenkins X」とは? 〜前編〜」からの転載です。



2018年3月に Jenkins を開発している CloudBee から「Jenkins X」というプロダクトが発表されました。

jenkins.io

社内で検証する機会がありましたので、この記事では前後編に渡って、CI/CD の解説から、Jenkins X を実際に使うまでに必要となった道のりや、使ってみての感想を書いていきたいと思います。

CI とは?

CI とは、Continuous Integration (継続的インテグレーション)の略です。
インテグレーション、と一言で言われても初めて聞く人にとっては何のことやらわからないと思いますが、アプリケーション開発中にエンジニアが頻繁に実施する以下の様なことを指します。

  • ビルド
    • ex.) コンパイルしてエラーなくバイナリなどの成果物を得る
  • テスト
    • ex.) 開発したアプリケーションをテストするコードを作成し、テストに記載された通りの動きになることを確認する
  • lint
    • ex.) 開発チーム内で予め決めたコーディングルールに則って、コードが書かれていることを確認する

ある機能を開発する際に Git などのブランチを新規に作成してから開発する、という昨今では当たり前のフローを取っていた場合、インテグレーション作業を以下の様なタイミング・内容で行いたくなるはずです。

  • master ブランチにブランチがマージされるたびに、ビルド、テスト、lint の実行結果が正常であることを確認したい
  • ブランチを切ってコードをコミット・プッシュするたびに、逐一ビルド、テスト、lint が正常であることを確認したい
  • テスト実行時には、モックではなく本番と同様のミドルウェアへ接続した上で、統合テストを実施したい

このような作業は、作成するアプリケーションの品質を向上させる効果があるため、可能であれば全て実施するべきです。

しかし、これだけの量の作業を毎度開発する人たちが手動で実行するのはとても大変です。
人間が手動で全てを実行する場合、開発に利用している端末のリソースを取られるので開発が止める必要があるかもしれません。また手動で実行しなければならないので、実行すること自体をそもそも忘れてしまうかもしれません。統合テストを実施するために、毎回環境を準備する作業も必要になります。テストを実行する環境(OS のバージョン、ミドルウェアのバージョン…)が厳密には揃っておらず、実行結果が異なるものになるかもしれません…などなど

これらの作業を自動化して、開発スピードの向上、成果物の品質向上を継続的に実現できるようにするものが、CI です。

CI を実現するプロダクトは本記事でも紹介する Jenkins や、SaaS としてサービスしている Travis CICircle CI など様々なものがあります。

CD とは?

CD とは、Continuous Delivery (継続的デリバリ)の略です。
前項で説明した CI を実施しているアプリケーションに対して、さらに頻繁なデリバリ(リリース)の実施作業を追加することによって、以下の様な効用が得られる、というソフトウェア開発における手法の1つです。

  • 一回のリリースでの変更点が少なくなり、リリース作業のリスク、リリースに伴うコスト、時間を抑えられる
    • リリース作業の自動化が実現している = リリース作業の定型化ができているとなるため、コスト・時間を大幅に抑えられます
  • アプリケーションを任意のタイミングでリリースできるようになる

アプリケーションのデリバリを頻繁に実施できるようにするためには、そのアプリケーションに関して CI が実施できていることが必須条件となり、それに加えて開発環境や本番環境などの異なる環境へのデプロイも自動化、または人間が判断できるタイミングを設けて、方針の決定後に素早く実行できる状態にしておく必要があります。

上記のような CD を実現するプロダクトは、Jenkins のほかに、SpinnakerGoCD などがあります。

Jenkins とは?

最初は Hudson という名前で Sun Microsystems の配下で OSS として開発されていましたが、Sun Microsystems がオラクルに吸収された後の 2011 年に Jenkins という名称でフォークし、現在は CloudBee の配下で OSS として活発に開発されています。

Jenkins は CI を実現するプロダクトとして人気を博しており、以下のような特徴を有しています。

  • Java Servlet アプリケーションとして開発されており、war ファイル 1 つで起動できる
    • docker イメージも存在し、公式にメンテナンスされているため、起動は本当に簡単です。
  • Jenkins 起動後に設定した内容は 1 ディレクトリ内に全て収まるため、バックアップが非常に用意しやすい
  • GUI 操作によるジョブの設定やテストレポートの表示が可能
  • 設定するジョブに対して、柔軟な設定が可能
    • ex.) リポジトリが更新されたら/X時間ごとにジョブを実行する、ジョブ実行後にメールや Slack で結果を通知する、など
  • 豊富なプラグインによる機能拡張が可能
  • Jenkinsfile によるジョブのコード化(Infrastructure as Code)を実現可能
  • ジョブの設定次第で CD ツールとしても運用可能
  • 動作が非常に安定している
    • 筆者の経験上の話ですが、ジョブの量が多かったり、プラグインが大量にインストールされていても、Jenkins 自体が原因で動作が不安定になったり遅くなったりしたところに遭遇したことがありません

Jenkins を CD ツールとして利用した場合の難点

上記で紹介した通り Jenkins は非常に柔軟で、CD ツールとしても利用可能ではありますが、実施したいことは自前でプラグインなりスクリプトなりを組み合わせてジョブとして設定する必要があります。

昨今流行している Kubernetes 環境への CI/CD を Jenkins で実現しようとした場合、以下の作業を自分たちで設定する必要が出てきます。

アプリケーションの開発者の範疇では、アプリケーションのビルド・テストまでは考えることができても、それ以外の項目を準備するのは非常に工数がかかる作業となってしまいますので、できればあまり手をかけずに準備できないものかと考えるはずです。

上記の問題を解消すべく、CloudBee が新たに発表したものが今回ご紹介する「Jenkins X」です。

Jenkins X とは?

Jenkins X は、以下のような環境でアプリケーションを開発する場合に限り、従来自前で考えて自動化しなければならない箇所をある程度簡単に実現できるようにしてくれます。

Jenkins X には、従来の Jenkins に加えて以下の機能が標準装備されています。

  • Jenkins 以外に CD として扱う上で必要となるツールのインストール
  • Git/Docker/Kubernetes を扱うためのプラグイン・Jenkins 設定のプリセット
  • Jenkins X を CLI から叩けるようにした jx
    • Kubernetes クラスタ・Jenkins X が動作する環境構築から、Jenkins X に設定したプロジェクトのビルド状況を確認するところまで CLI でできます

まとめ

本記事では、CI/CD 自体の解説、必要性から Jenkins/Jenkins X の概要について簡単に説明しました。

次回以降、実際に Jenkins X をインストールする手順を解説しつつ、何がインストールされるのか、どういう形で動くのかを記載していきたいと思います。

アプリケーション開発におけるロックの重要性と ORM におけるロックの実現例

こちらは 「アプリケーション開発におけるロックの重要性とORMにおけるロックの実現例」からの転載です。



アプリケーション開発においてデータを扱う時にロックを行うことはデータ保全性の観点から重要です。(もちろん SEROKU フリーランスでもシステム側ではデータのロックを行っています)

本記事では、一般的な話としてアプリケーションの「ロック」について掘り下げ、ロック方法の種類や DB のトランザクションとの関連性を紹介します。

次回以降は、楽観的ロックについてと、ORM を使ったロックの実現方法をアプリケーションフレームワークごとに紹介する予定です。

ロックとは

まず、ロックとはデータに対して読み書きの処理を同時に行うことを排除する仕組みです。

ロックを行わない場合、同じデータに対して複数の処理を実行すると、結果に整合性が取れなくなったりします。

例えば、商品カテゴリごとの売上の合計を計算する処理が実行されている間に、商品カテゴリが変更されると、商品カテゴリごとの売上の合計は総売り上げと違う値となってしまいます。

そこでロックを用いることで、一方の処理が完了するまでもう一方の処理の読み書きを禁止させることができ、結果の整合性を取ることが出来ます。

ロックを掘り下げる

システムの「どのレベル」でロックを行うかにより、考慮すべきポイントやロックできるデータの単位が異なります。レベル毎に掘り下げていきます。

DB レベルのロックとアプリケーションレベルのロック

DB には MySQL, PostgreSQL, Oracle, Microsoft SQL Server, MongoDB, Cassandra, Redis 等幾つか種類がありますが、ほぼ例外なくロック機能を有しています。

DB でロックを行う場合 DB によって詳細な挙動は異なりますが、読み書き(RW)を同時に排除するロック、書き込みのみを排除するロック等、「どの操作」に対してロックを行うかを決めることが出来たり、RDB におけるテーブルに対する操作を排除するロック、レコードに対する操作を排除するロック等、「どのデータ単位」に対してロックを行うかを決めることも出来ます。

一方で、アプリケーション内で行われる複数の処理において、それぞれ共通のルールを持たせることでロックを実現することが出来ます。(ルールを破る処理が無い前提)

アプリケーション全体で共有できるメモリ空間にロック有無を示す boolean 変数を用意し次のルールを設けることでロックが行えます。

  1. DB へ読み書きを行う際は必ずその変数を参照する
  2. ロック有無が false であれば変数を true にして DB へ読み書きを行い、終わったタイミングで変数を false に戻す
  3. ロック有無が true であれば変数が false となるまで待機する

トランザクションについて

トランザクションとは DB に対する一連の操作を一つの処理単位として扱う機能です。 処理の途中で失敗した場合は、操作を開始した時の状態にロールバックされます。 処理の完了はコミットと呼ばれます。

並列でトランザクションが実行される場合に、操作の一貫性を保てるようにするためにはロックが必要となります。 この時、どのような処理をロックするかをトランザクションの分離レベルと呼び、DB 毎に分離レベルがいくつか用意されています。

例えば、MySQL の分離レベルは次の種類があります。

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

それぞれ簡単に説明します。

分離レベルを READ UNCOMMITTED に設定すると、トランザクション中に変更された非コミットな値の読み込みが可能です。

トランザクションを2つ実行した際に、一方で値の変更を行ったもののロールバックされた場合、もう一方で変更後の値が読み込まれると結果の整合性がとれない状態となる可能性があります。

分離レベルを READ COMMITTED に設定すると、コミットされた値が読み込まれるようになります。

しかし、トランザクションを2つ実行した際に、一方で値の変更が行われ、もう一方で同じ値を参照していたとした場合、値を変更するトランザクションが完了次第、もう一方のトランザクションからは変更後の値が参照されることになります。

そのため、値の参照を行う側のトランザクションを考えると、トランザクションの途中から同じ値の呼び出しを行った結果が異なることとなります。

分離レベルを REPEATABLE READ に設定すると、トランザクション中に何度値を読み込んでも結果が変わらないことが保証されます。

最後に、分離レベルを SERIALIZABLE に設定すると、トランザクションは並列に行われず順次実行されるようになります。

トランザクションシステムの信頼性を高める特性

トランザクションを行うことができるシステムにおいて、その信頼性を高めるために実現するとよいとされる特性があります。
特性の種類としては、トランザクション自体についての特性を示す ACID 特性、システム自体の特性を示す BASE 特性があります。

システム自体の特性として ACID 特性を持たせようとすると、可用性や性能が低下するトレードオフとなることから、可用性や性能を重視した BASE 特性が Eric Brewer により提唱されました。(参考情報: yohei-y:weblog: CAP と BASE について調べたこと)

ACID 特性と BASE 特性は共存可能なので、DB は ACID 特性を保ち、システムは BASE 特性に従うといった構成をとることができます。

ACID 特性

ACID 特性とは、次の4つの特性を指します。

  • 原子性(Atomicity)
  • 一貫性(Consistency)
  • 独立性(Isolation)
  • 永続性(Durability)

それぞれ簡単に説明すると、次のとおりです。

  • 原子性とはトランザクション処理が一連の処理「全てが実行された」か、あるいは「全てが実行されなかった」(実行前の状態に戻す) ことを保証する特性のこと
  • 一貫性とはトランザクション処理があらかじめ決められたルールに従って整合性が保たれていることを保証する特性のこと
  • 独立性とはトランザクション処理の途中経過が隠蔽できる特性のこと
  • 永続性とはトランザクション処理が完了したことをユーザに通知した時点で、その操作が永続化されたことを保証する特性のこと

詳細については ACID (コンピュータ科学) - Wikipedia 等を確認してみてください。

BASE 特性

BASE 特性とは、次の特性を指します。

  • Basically Available
  • Soft-State
  • Eventual Consistency

それぞれ簡単に説明すると、次のとおりです。

  • Basically Available とは可用性が基本である特性のこと
    • ロックにより処理が待機されることがないことを指します (楽観的ロック)
  • Soft-State とは緩いステート管理を行う特性のこと
  • Eventual Consistency とは途中で整合性が保たれていなくても、結果的に整合性が保たれていればよしとする特性のこと

まとめ

今回はロックについて掘り下げ、トランザクションについてとその特徴を紹介しました。

次回は、楽観的ロックについて紹介したいと思います。

Angular Tips その2

こちらは 「SEROKUを支える技術 Angular Tips編 その2」からの転載です。

本記事では SEROKU の開発を例に Angular Tips の紹介をしています。(その 3 くらいまで続いています)



やってまいりました、SEROKU を支える技術 Angular Tips その2。

前回の記事執筆からほぼ1ヶ月。待ってくれていた人、そうでない人、いるかとは思いますがめげずにまた書いていこうと思います。

その1でも書きましたが、あくまで、こう書くべきだという強い意志ではなく、こう書いたら良いんじゃないかな? くらいの気持ちで書きますので参考程度にしてらえれば幸いです。

さて、今回は特段前置きは不要かと思いますので(必要あらば前回の記事をサクッと見てください)いきなり書いていこうと思います。

Tips 3: ng-template と ng-container の使い分け

Angular v4 になって、ただの template タグが ng-template にほぼ置き換わるような形になり、angular tepmlate のような文字列でグーグル先生に問い合わせたときに ng-template もしくは ng-container で書かれたサンプルがヒットしたりなんかしてどっち使えばいいか混乱しませんか?(私は結構しました)

混乱の元はなにかといえば、

  • どちらもタグ自身は吐き出される HTML には出力されない
  • ngIf, ngFor 構文が使える(ただし書き方にちと作法がある)
  • ng- の prefix を持っている
  • どちらの書き方をしても見た目には同じ表示になる

といったところでしょうか。ではサンプルソースを元に検証していきたいと思います。

ngIf を ng-template と ng-container で書いてみる

  • ng-template の場合
<ng-template [ngIf]="評価したいもの">
    ・
    ・
    ・
</ng-template>
  • ng-container の場合
<ng-container *ngIf="評価したいもの">
    ・
    ・
    ・
</ng-container>

ngIf の書き方が違うことに注意してください。ng-container の場合は * による ngIf で書けますが、 ng-template の場合はその記法で書くとただのコメントかのような振る舞いになってしまうので注意が必要です。

ngFor を ng-template と ng-container で書いてみる

  • ng-template の場合
<ng-template ngFor let-ローカル変数名 [ngForOf]="forで回したい変数">
    <反復して表示させたいタグ>{{ ローカル変数名でアクセス可能 }}</反復して表示させたいタグ>
</ng-template>
  • ng-containerの場合
<ng-container *ngFor="let ローカル変数名 of forで回したい変数">
    <反復して表示させたいタグ>{{ ローカル変数名でアクセス可能 }}</反復して表示させたいタグ>
</ng-container>

こちらも ngFor(of) の書き方が違うことに注意してください。

単純な書き方の比較だけをすると、ng-container のほうが通常のタグと同じような構文で書けますね。 なので ng-container を使いましょう!笑

と書きましたが、ちょっと説明が短絡的すぎですね(; ・`д・´)

もう少し詳しく書くと

そもそもなのですが、やはり役割が違います笑

単純なクラスの話だけをしても TemplateRefViewContainerRef で分かれていますが、名前の通り ng-template はただのテンプレートとしての機能です。 ngIf/then/else 構文 の説明のときに、then または else に指定したのは、まさにこの ng-templateTepmlateRef そのものだったのです。

一方、 ng-container は then/else には指定できません。ベースのクラスが違うからと言うのが一番の理由ですが、使いみちがやはり違うということでしょう。 ちなみに、ng-template で定義したテンプレートを ng-container の view として出力することができます。コードで見てみましょう。

  • ng-contaier の中身を ng-template で定義したもので表示する
<!-- テンプレートを定義-->
<ng-template #sample>
    <div>
    サンプルのテンプレートです。    
    </div>
</ng-template>

<ng-container *ngTemplateOutlet="sample"></ng-container>

これだけで見たら、ngIf(もしくは ngSwitch )の出し分けで事足りるわけですが、このように ng-template で定義したものを動的に出し分けることが可能なので複雑なロジックが絡む場合はうまく組み合わせると良いかもですね。

別の観点か

実装のお話をもう少しすると、ngIf, ngFor は同一のタグに対しては設定できません。 どういうことかというと、以下のようなコードはエラーになります。

  • ngIf, ngForを書きたい
<div *ngIf="条件" *ngFor="ループ">
・
・
・
</div>

なので以下のように書くこともできますがそうすると、外側に書いた div タグが無駄に出力されてしまいます。

  • ngIf, ngForを書きたいのでタグ分けるサンプル
<div *ngIf="条件">←書けるけどこのdiv本当は不要
    <div *ngFor="ループ">
        ・
        ・
        ・
    </div>
</div>

ここで使えるのが ng-container です。(前述の通りng-templateでも書くことはできますがng-containerで書くことをお勧めします )

  • ngIf, ngForを書きたいのでタグ分けるサンプル( ng-container を使う)
<ng-container *ngIf="条件">
    <div *ngFor="ループ">
        ・
        ・
        ・
    </div>
</ng-container>

こう書くことで出し分けをしつつ、無駄なタグが出力されるのを防げます。

*ngFor の内部に *ngIf のような場面でも活躍しますね。(一定のループ処理で表示したいときに一部分だけ特定の条件下で表示/非表示を切り替えたい時など)

あと、副次的な作用として、 ngIf/ngFor を使うときに ng-container を使うことをルール化すればコードリーディングの観点からも見通しが良いかもしれません。( ng-container が書いてある箇所は何かしらの出し分け処理が挟まっているとパット見でわかる)

以下のようなコードは ngIf, ngFor が div や ul, li タグ、はたまた ng-container で色々使われていてなかなかカオスですね(^^;

脳内変換力が試されるようなコードはなるべく避けましょう笑(数カ月後、自身ですらわからなくなる可能性あり!!)

  • 無秩序な ngIf, ngFor を使ったサンプル
<div *ngIf="条件その1">
    <ul>
        <li *ngFor="ループ"></li>    
    </ul>
</div>
<ul *ngIf="!条件その1">
    <ng-container *ngIf="条件その2">
        <li *ngFor="ループ"></li>    
    </ng-container>
</ul>
<ng-container *ngIf="条件その3">
    ・
    ・
    ・
</ng-container>

まとめ

今回は ng-template, ng-container に関して書きました。

次回は ng-content を使った、テンプレートに対して外部から子要素を突っ込むような処理を書いてみようかと思います。 それでは皆さんごきげんよう

関連記事

tech.weseek.co.jp

自作で Video Chat 環境を作って導入した話 〜経緯編〜

こちらは 「自作で Video Chat 環境を作って導入した話 〜経緯編〜」からの転載です。

弊社では九州の大分県サテライトオフィスがあります。そのサテライトオフィス開設時に行った Video Chat 環境の検討経緯を紹介します。



「SEROKU フリーランス(以下、SEROKU)」の中の人をやっている ryosuke です。 今回は SEROKU の裏側の話から離れて、社内に Video Chat システムを導入した件について取り上げたいと思います。

初の会社拠点追加!どうやって開発業務を回す??

WESEEK の本社は東京の飯田橋にあります。殆どの社員は本社に出勤して開発業務を行っています。今回、新たに九州の大分に在住している方を社員に迎え入れることになりました。これに伴い、新たに別府オフィスを構えました。

さてここで懸念事項にあがるのが 「距離が離れた社員を交えても円滑に開発業務を行うことができるのか?」 という点でした。今回、別府オフィスを構えたのは、別府にいる人材を社員に迎え入れるのが目的でした。つまり「別府は別府で案件取ってきてそっちだけでよろしく開発してねー」ではなく、本社の社員と同一のプロジェクトに一緒に携わることを想定してオフィスを構えています。(たとえ別府付近の企業から案件を受注したとしても、別府オフィスの社員だけでなく本社の社員も含めてプロジェクトを回すことになるでしょう。)元々、WESEEK 社員は全員が本社に出勤し、同一の空間で集まった状態で開発をしています。東京にある本社と九州にある別府オフィス、当然ながら空間的な距離を埋めることは不可能なので、「同一の空間に集まって業務」は望めません。

WESEEK では業務のコミュニケーションツールとしては slack を使用しており、開発に関する会話は slack 上で行うことが殆どです。「みんな slack で話をしてるんだったら、どこで仕事しようが同じじゃない?」と思うかもしれませんが、slack 上での議論が込み入ったり、すぐに知る必要がある情報を尋ねるときは対面で会話した方がよいこともあり、全てを slack に頼るのにも不安が残ります。また、本社で対面で行った会話は、当人が明確な意思を持って slack などのコミュニケーションツールに会話の内容を転記しないと別府オフィス側には伝わることがなくなってしまいます。

上記のように想定されるコミュニケーションロスによる業務進行への悪影響を最小化すべく、まずは遠隔地であることを理由としたコミュニケーションの手間をできるだけなくす方法を検討しました。

いろいろあるコミュニケーション方法

コミュニケーションの手段はいろいろありますが、まずはそれぞれの手段における長所、短所を整理してみましょう。

対面での会話

まずは対面での会話です。基本的なコミュニケーション手段ですが、リアルタイムで会話を進める方法の中では情報の出力スピードが(例えばキーボード入力や手書きの文字などに比べると)早く、会話のキャッチボールのスピードとしては最速の部類に入ります。また会話の内容そのもののやりとりに加え、聴覚、視覚から受け取ることができる文字には含まれてない情報も知覚することができます。例えば声のトーンや言い直しがあったり、表情や身振り手振りなどがそれに当たるでしょう。これらを読み取ることで、発言などをとおして、自信満々なのか自信がないのか、不安に思っているのか、興味があるのか、ないのか、そもそも話を聞いていないのか、聞いていないにしても興味が無くて聞いていないのか、他のことで忙しそうで会話に参加する余裕がないのか等々、知ることができる情報が多くあります。会話の内容に加え、これらの情報も鑑みて会話中での応答の仕方を変えることもあるでしょう。また、声や身振り手振りだけで情報が伝えるのが難しい場合でも、ノートやホワイトボードを使って、その場で図を書いたり、論点を字に起こして整理しながら進行することもできます。

もちろんいいことばかりではありません。対面での会話は物理的に同一の空間に居合わせる必要があります。またリアルタイムで参加していないと会話の内容をキャッチアップすることができません。さらに会話に参加する場合は、会話以外の作業は一旦とめて会話に集中する必要があります。なぜなら対面での会話はその場ですぐ記録はされないので、会話に参加する以上はリアルタイムで内容を聞き続ける必要があります。会議などでも(議題の濃さにもよると思いますが、)内職していたり、ちょっと別のことを考えているとあっという間に置いてけぼりになったり、聞いていなかった期間の内容が欠落した状態で会話に参加することになります。従って、会話に参加して欲しい人全員の都合を同タイミングで拘束する必要があり、かつ人数が増えるほど会話するためのコストが膨らむ手法でもあります。また、作業に集中している人に話しかける様な場合は、相手に対し割り込みでコンテキストスイッチをしてもらう必要があるので、その切り替えによって元々の作業の手戻りが発生したり、再度集中するまでに時間がかかるなどの相手側にコストを強いる場合もあります。

今回は遠隔地にいる社員とのコミュニケーションを考える必要がありますから、対面の会話は取り得る手段から除外せざるを得ません。

手紙

それでは遠隔地とのコミュニケーション手段を考えていきましょう。まずはローテクな手紙(郵便)によるやりとりから考えていきましょう。伝えたいことを紙面上に文字にして、その紙を物理的に送りあいます。この手段なら遠隔地にいてもコミュニケーションが成り立ちますが、東京-大分間だと相手に情報が伝わるのに 1 日以上かかってしまい、リアルタイムでの会話にはほど遠いですね。採用には至らないでしょう。

遠隔地でリアルタイムのコミュニケーションをするには電気・電子の力に頼る必要がありそうです。(そりゃそうですね)

電話・Voice Chat

では電話はどうでしょうか?ここでは音声のやりとりができるツール(skype, LINE など)での Voice chat も電話に含めることにしましょう。まず遠隔地にいてもリアルタイムに会話ができます。また音声で伝えられるので対面での会話に近いスピーディーなやりとりが望めそうです。

一方で、相手の表情や身振り手振りなど視覚的な情報は伝えることができませんので、対面の会話ほど情報量は充実していません。また、音声のやり取りもマイク・スピーカーや DSP の性能に品質が依存します。品質が低いと、相手が何を話しているか伝わりづらくなり電話の利点を活かせなくなります。品質の高い機器を取りそろえようとすると特に初期導入時のコストも膨らみます。また、対面での会話と同様に会話を成り立たせるには相手の時間を拘束する必要があります。

電子メール

ではリアルタイムで情報を伝えつつ、相手の時間を強制的に束縛しなくて済む方法も検討してみましょう。電子メールはどうでしょうか?電子メールならば文字で伝えたいことを書き相手に送信することで、ほぼリアルタイムで遠隔地の相手が情報を受け取ることができます。また、受け取る側は自分の都合が良いときに届いたメールを読むことができるので、相手の作業に割り込むことなくコミュニケーションを取ることができます。また、対面での会話や電話と比べると特に特別なことをしなくても(メールを削除しなければ)あとでどういう会話をしたか見返すことも比較的容易です。

一方で、すぐに返信が欲しい場合は適した手段ではなさそうです。お互いに手が空いていて会話の内容に高い関心がある場合は、頻繁にメールをやりとりすることで会話が捗りますが、基本的にはメールを送信したからといって相手がすぐに読んでくれるとは限りません。送った側からすれば、もうメールを読んでくれたのか、まだ読んでいないのか、読んだとしたら今返信しようとしているのか、それとも返信するために調べ物をしているのか、それとも重要ではないと判断されて返信を後回しにされているのか知ることもできません。急いでいるときはやきもきしてしまうでしょう。また、伝える側も電話や対面での会話と比べれば文書を書き上げるのにはそれなりの時間を費やす必要があります。(タイピングの早さに依存します。)

チャット

(文字ベースの)チャットツールはどうでしょうか?これは電子メールと近い性質をもっています。こちらの都合の良いときに問いかけつつ、相手も都合の良いときにメッセージを確認して返信することができます。過去の会話を見返すこともできます。また、業務で採用されるチャットツールの傾向として 1 対 1 の会話ではなくチームで会話することを前提としています。従って、相談や疑問点をしたいときに特定の人に問いかける形でなくても、それに対して答えを持つ別の人が代わりに回答してくれることもあります。また、特定の誰かに問いかけたいときはチャットツールのメンション機能を使用することで特定の人に向けて問いかけていることを明示することもできます。(もちろんこの場合でも、手の空いている他の人が回答することができます。)また、メールとは異なり、相手が反応できる状況にあるかをある程度知ることができます(チャットへの参加状態の表示や、文字入力中であることを表示できたり、LINE のような既読通知機能など)。さらに(これは機能と言うより性質から来る文化的な違いといえますが、)

一方で電子メールと同様にすぐに返信してくれるかわからない(既読無視などもあり得る)、文章をタイピングするのにそれなりに時間がかかるなどデメリットがあります。更にチャットツールではあるチャンネル(チャット部屋)で同時に複数の異なる会話が行われた結果、内容が混線してしまうこともあります。混線はチャンネルを細分化すれば防げますが、あまりに分割しすぎると今度はチャンネルが膨大な数になり、どこのチャンネルですべき話題か、どこのチャンネルでした話題だったかを探すのが大変になってしまいます。

Video Chat

Video Chat はどうでしょうか?電話・Voice Chat の機能に加え、映像もリアルタイムに伝えられるようになりますので、電話の上位互換な手段と言えます。電話よりもさらに対面での会話に近いやり取りが望めそうです。例えば、電話ではつかめなかった相手の表情もわかりますし、言葉だけでのやり取りでは伝えにくい部分があればノートやホワイトボードを使ってその場で図を書きながら共有することもできるでしょう。また、PC の画面共有機を持つツールもあるので、議論となっている画面をお互いに見ながらやり取りすることもできます。

一方で、映像を HD でも送受信できる時代になったとはいえ、対面と同等の解像度や画質を実現できているわけではありませんので、対面と比べれば認識できる情報は劣るでしょう。もちろん画質も(電話と同様に)音質の良し悪しも機器の性能に依存しますので、品質を高くしようとするとコストに跳ね返ります。音声に加え、映像の品質も上げようとするとその分、電話よりもコストがかかりそうです。また、対面での会話と同様に会話をするのに相手の時間を拘束する必要があります。

WESEEK 的なコミュニケーションのいいとこ取り

上記にいくつかのコミュニケーション手段に対し、長所・短所を挙げました。特徴を整理すると、電子メールとチャットは文字コミュニケーションベースのコミュニケーションであり、一方で対面での会話と電話、Video Chat は音声(+映像)でのコミュニケーションという性質を持っていますね。

さて、実際に遠隔地の社員と円滑に業務を遂行するためのコミュニケーション手段は何を採用するのが良いのでしょうか?

現行の本社内でのコミュニケーション手段については重大な問題なく円滑にできている認識です。したがって、本社内でとっているコミュニケーション手段と同等、またはそれに近い手段を遠隔地に対してもとることができることを目標に手段を選ぶのが良いと考えました。

WESEEK 社内では主なコミュニケーション手段はチャットと対面での会話が使われていると思います。基本はチャット上でやり取りすることで相手の作業に割り込こんで止めてしまうコストを減らしつつ、かつ議論もチャットログを見返せば途中から参加することもできます。しかし急ぎで応答が必要な時、特に答えや結論が出るまで業務が止まってしまうような場合にチャットで問いかけても、相手の反応がないく仕事にならない場合もあります。優先度の高い業務の場合はその日がいわそんな時は対面での会話が有効です。相手の作業を一時的に止めるコストを支払ってでも会話を行うほうが結果として全体の業務が滞らずに進む場面もしばしば見受けられます。このように WESEEK ではチャットと対面での会話という手段を使い分けて、いいとこ取りのコミュニケーションを行っています。

チャットツールは slack を使っており、これを別府オフィスの社員に使ってもらうことは容易です。したがって、文字ベースのコミュニケーション手段ではわざわざ既存の方法を変えるまでもなく slack を使ったチャットを引き続き使えばよさそうです。これにくわえて「対面での会話」に相当する代替手段を用意する必要があります。近い性質を持つ手段の中では Video Chat が最も対面での会話に近い品質を提供できそうです。その分、コストがかかるものの、今回はコミュニケーションロスによる業務進行への悪影響を最小化するという点を重点に置き、 Video Chat をコミュニケーション手段に加えることにしました。

まとめ

今回は新拠点の設立をきっかけとして、Video Chat を導入するに至った経緯についてお伝えしました。次回は、実際に導入したサービスと機器の選定などについてお伝えする予定です。