QUO CARD Digital Innovation Lab Tech Blog

クオカード デジタルイノベーションラボの技術ブログです

java.time.LocalDateTime と時差について

こんにちは、日本のテトリスプレイヤーの上位 1% に入る飯島です(デジタルイノベーションラボ所属)。 日付を扱う時によく使用される java.time.LocalDateTime ですが、意外と時差のことを気にせず使っている方も多いのではないのでしょうか。今回は LocalDateTime と時差周りの話、その他 java.time に含まれるクラスの話をしていきます。

LocalDateTime について

LocalDateTime は時差情報を持っていません。 つまり、2020-11-24T12:00:00 を表す LocalDateTime があったとして、それがいったいどの地域に置ける時刻なのかは不明だということです。 日本における 11/24 12:00 はイギリスでは 11/24 3:00 なのに、LocalDateTime ではそれを表すことができません。 日本のシステムと海外のシステムを API 連携するとき等に LocalDateTime でやりとりしていると不整合が出てきてしまいます。 クラウドのリソースを使っていて、アプリケーションサーバーのタイムゾーンと DB サーバーのタイムゾーンが異なっている時も不整合が出てきてしまいます。

不整合が発生する例

弊社では ORM に jOOQ を使うことが多く、jOOQ の java コード生成機能でテーブルに対応するクラスを作って DB を操作するようにしています。 テーブルが timestamp 型のカラムを持つ場合、生成されたコードでは LocalDateTime が対応づけられます。 この場合、アプリケーションサーバーが UTC+9:00, DB サーバーが UTC+0:00 などオフセットが異なっていても、アプリケーションサーバーで作成した日時が DB に保存されます。 例えばアプリケーション側で LocalDateTime.now() をして 2020-12-21 11:44:49.288105 が生成されると、DB にもそのままの形で保存されます。 この値を UTC+0:00 などの別アプリケーションから読み込むと、元々の時差分がずれた日時が取得されてしまいます。

※ timestamp with time zone 型を使うことで時差に対応することは可能です。ただ、自分たちで設計した DB ではない場合など、timestamp 型が前提となっている場面は多々あるかと思います。

じゃあ何を使えば良いのか

基本的には java.time.Instant か java.time.OffsetDateTime を使えば良いかと思います。 LocalDateTime が求められる場面では、LocalDateTime.ofInstant(instant, ZoneOffset.UTC) や LocalDateTime.now(ZoneOffset.UTC) などで 変換するのが良いでしょう。

Instant

時系列上のある一点を表すクラスです。 時差は持っていませんが、LocalDateTime が時系列上の点を表わせないのに対して、こちらは UTC に固定された一点を返します。 試しに手元で現在時刻を表示させてみると、こうなります。

> Instant.now()

2020-11-30T08:35:16.088031Z

画面に Asia/Tokyo の時刻を表示したい場合など、ユーザーが Asia/Tokyo を望んでいる場合は、アプリケーション側で変換して出力すれば良いです。

OffsetDateTime

標準時からの時差情報を含んだデータです。 UTC+8:00 の 13:00 や UTC+9:00 の 15:00 といったデータを持っています。

> OffsetDateTime.now()

2020-11-30T17:35:16.101819+09:00

※ OffsetDateTime#now はシステムのデフォルトタイムゾーンを使用して計算されるため日本の日時が表示されていますが、情報として UTC+9:00 が含まれているため時系列上の一点を表せています。

ZonedDateTime

国・地域ごとの時差情報を持った時刻データです。 1 年を通して常にオフセットが固定の地域もありますが、サマータイムにより 1 年のうちでも標準時を 進める/戻す などオフセットが変わる地域も存在します。 ZonedDateTime ではそういった地域の時差情報を表すことができます。

現在の日本は一年を通してオフセットは一定(UTC+9:00)ですが、実は過去にサマータイムを導入していた時期がありました。 下記コード (kotlin で書いてます) でも確認できます。 1948 年 5 月 2 日の 0:00 ~ 1:00 が存在しないことがわかります。 オフセットも 9 時間から 10 時間になっていますね。

val zonedDateTime = ZonedDateTime.of(1948, 5, 1, 20, 0, 0, 0, ZoneId.of("Asia/Tokyo"))
(1..10).fold(zonedDateTime) { dt, i ->
  dt.plusHours(1).also { println(it) }
}

1948-05-01T21:00+09:00[Asia/Tokyo]
1948-05-01T22:00+09:00[Asia/Tokyo]
1948-05-01T23:00+09:00[Asia/Tokyo]
1948-05-02T01:00+10:00[Asia/Tokyo]
1948-05-02T02:00+10:00[Asia/Tokyo]
1948-05-02T03:00+10:00[Asia/Tokyo]
1948-05-02T04:00+10:00[Asia/Tokyo]
1948-05-02T05:00+10:00[Asia/Tokyo]
1948-05-02T06:00+10:00[Asia/Tokyo]
1948-05-02T07:00+10:00[Asia/Tokyo]

国・地域ごとの時差情報は、デフォルトでは tz database から取得するようになっています。


以上、java.time.LocalDateTimeは時差情報を持っていないので使うときは気をつけましょうという話でした。 それでは楽しい時差ライフを!!

CIのテストを修正した資産によって分けて実施する

こんにちは、クオカード デジタルイノベーションラボの鳥海です。

QUOカードPayオンラインストアは内製化に切り替えてからもう少しで半年ほど経ちます。
内製化に切り替えた後は、新たな機能追加とリファクタリングを進めており、それぞれで unit test が作成できるものは作成し、CI による自動テストを実施するようにしています。
CI には GitHub Actions を使用しています。

自動テストを修正のたびに都度実施することにより、安心して既存の機能が改修ができたり、リファクタリングが行えるようになったのですが、テストソースが増えるにつれて以下のような問題が発生してきました。

  1. 一部のモジュールの修正しかしていないのに、全てのテストを実施しており無駄な待ち時間や 余計な GithubActions のリソース消費が発生していた
  2. たまに実行中のまま止まってしまうテストがでてきた

1 に関しては、静的資産(HTML、CSS、JS など)のみ修正した場合など、不要な場合もテストを実施していたので、こういう場合は実施しないようにすべきでした。
2 に関しては本来、事象を解析して直したいところでしたが、作業の優先度も考慮し該当のテストを無効にして対応していました。ただ今後その機能が修正された場合はテストを実施すべきであるのと、担当者内で無効にしていることを共有できているうちはよいのですが、新しいメンバーが増えた際に伝達漏れなどが発生し得ると思いました。

上記の点を解決するために、毎回テストを一律実施するのではなく、修正した資産によってテスト絞って実施できないか調べてみました。

GitHub Actions のテストワークフローの実施を GitHub に push したパスによって絞る

GitHub Actions のpathsを使うと、修正したリソースパスを指定してワークフローが実行できるようになります。

下記のようにパスを指定すると push した際に差分 が発生した資産がそのパスに含まれていれば、ワークフローが実施されます。
また、指定したパスの中で除外したいものがあれば!を付与したパスを下行に追加することにより除外できます。
パスの指定方法にはワイルドカードが指定可能です。 指定方法の詳細はここを参照ください。

GitHub Actions のワークフローファイル

on:
  push:
    paths:
    - 'module1/**'
    - '!module1/function1/**'

以下のようにワークフローを実施しない資産のパスを指定することもできます。

on:
  push:
    paths-ignore:
    - 'module1/**'

これで push した際にワークフローを実施する資産を特定できるようになったのですが、これだけだとテストごとにワークフローファイル作らなくてはいません。

ワークフローでテスト以外は Github リポジトリのチェックアウトや Java のセットアップなどがありますが、テストごとにファイルを分けると、これらは各ワークフローで重複することになり、これはよろしくないなと思いました。

そこで一つのワークフローファイルの中で修正された資産を判定し、テストを分けて実施できないか調べてみました。

テストワークフロー内で、修正した資産を判定できるようにする

現時点で GithubActions に用意されている機能ではワークフロー内で修正資産を判断できるものがないため、以下のプラグインを使用してどの資産に修正が入ったか判定できるようにしました。

dorny/paths-filter

このプラグインを使用して資産ごとにテストを分けたワークフローのソースが以下となります。

jobs:
  test:
    name: Run tests
    runs-on: ubuntu-latest

    -- 省略 --

    steps:
      # 修正された資産を判定する※
      - name: paths-filter
      uses: dorny/paths-filter@v2
      id: filter
      with:
        base: ${{ github.ref }}
        filters: |
          module1:
            - 'module1/**'
          module2:
            - 'module2/**'
          module_common:
            - 'module_common/**'

      # モジュール1のテスト
      - name: module1 test
        id: module1-test
        if: steps.filter.outputs.module1 == 'true' || steps.filter.outputs.module_common == 'true'
        run: ./gradlew module1Test

      # モジュール2のテスト
      - name: module2 test
        id: module2-test
        if: steps.filter.outputs.module2 == 'true' || steps.filter.outputs.module_common == 'true'
        run: ./gradlew module2Test

      # 共通モジュールのテスト
      - name: module_common test
        id: module_common-test
        if: steps.filter.outputs.module_common == 'true'
        run: ./gradlew moduleCommonTest

      # モジュール1のテスト結果をアップロード
      - name: store reports of module1
        uses: actions/upload-artifact@v2
        # テストが実行された場合だけアップロードする
        if: always() && steps.module1-test.outcome != 'skipped'
        with:
        name: module1-report
        path: module1/build/reports

      # モジュール2のテスト結果をアップロード
      - name: store reports of module2
        uses: actions/upload-artifact@v2
        # テストが実行された場合だけアップロードする
        if: always() && steps.module2-test.outcome != 'skipped'
        with:
        name: module2-report
        path: module2/build/report

      # 共通モジュールのテスト結果をアップロード
      - name: store reports of module_common
        uses: actions/upload-artifact@v2
        # テストが実行された場合だけアップロードする
        if: always() && steps.module_common-test.outcome != 'skipped'
        with:
        name: module_common-report
        path: module_common/build/report

ソースの※の箇所がプラグインを使用しているところです。 base: ${{ github.ref }}を指定すると、そのブランチのHEADとの差分を比較することになります。(指定しないと分岐元のブランチとの差分比較となります) フィルターを通すと、steps.filter.outputs.<フィルタ名>で修正した資産を判定して、それに応じてテストを実施できるようになります。

また、複数条件を指定することで判定文が長くなるため、テスト結果のアップロードでは同じ条件を指定せず、テストステップに id を付与し、その id のテストが実施されたかを steps コンテキストのoutcome を使用して判定しています。

これでテストをワークフロー内で呼び分けることができました。
次は実施するテストを分けます。

Gradle でテストを実施したい単位に分ける

クオカードペイオンラインストアではビルドツールに Gradle を使用しています。
Gradle では、以下のようにincludeexcludeを使用して、タスクの対象資産を特定することができます。

module1/build.gradle

task module1Test(group: 'verification', type: Test, dependsOn: testClasses) {
  useJUnitPlatform()
  # function1配下は除いたテスト
  exclude '**/module1/function1/**'
}

task function1Test(group: 'verification', type: Test, dependsOn: testClasses) {
  useJUnitPlatform()
  # module1のfunction1のみのテスト
  include '**/module1/function1/**'
}

上記で示した方法を柔軟にテストを分けて実施することができるようになります。
これで修正した資産に影響するモジュールのみテストを行うようにしたので、問題点を解消できました。

テストの並列化

テストは一つのジョブでなく、複数のジョブに分けて定義し並列で実施するようにもできます。
GithubActions では優先度が同じジョブは並列で実施されます。
ただし、各ジョブ間ではセットアップしたものなどのリソースを共有できません。
共通処理をそれぞれ行わなくてはいけなく、今回はそれに伴う時間と並列化により効率化される時間を比較して、結果的に並列化は実施しませんでした。

ただ dorny/paths-filter プラグインの判定結果をジョブを起動する条件にも使えるので、今後テストが増え、テストに大幅に時間がかかるようになった際は並列化をすることで時間の短縮が可能になると思われます。

以下のようにすると module1 と module2 のテストが並列で実施されます。

jobs:
  filter:
    name: Filter
    runs-on: ubuntu-latest
    outputs:
      # 変更した資産の判定結果を他のjobに渡す
      module1: ${{ steps.filter.outputs.module1 }}
      module2: ${{ steps.filter.outputs.module2 }}
      module_common: ${{ steps.filter.outputs.module_common }}
    steps:

      -- 省略 --

      - name: paths-filter
      uses: dorny/paths-filter@v2
      id: filter
      with:
        base: ${{ github.ref }}
        filters: |
          module1:
            - 'module1/**'
          module2:
            - 'module2/**'
          module_common:
            - 'module_common/**'

  test-module1:
    name: Run tests module1
    runs-on: ubuntu-latest
    needs: filter
    if: needs.filter.outputs.module1 == 'true' || needs.filter.outputs.module_common == 'true'

    -- 省略 --

  test-module2:
    name: Run tests module2
    runs-on: ubuntu-latest
    needs: filter
    if: needs.filter.outputs.module2 == 'true' || needs.filter.outputs.module_common == 'true'

    -- 省略 -

色々無駄なところを省くことによって、各メンバーがより効率的に作業ができますし、GitHub Actions の使用リソースも抑えられます。
最初は問題となっていなくてもプロジェクトの規模が大きくなっていくにつれて問題となってくるものもあるので、見直すことで最適化をはかり、非効率となっているものを取り除いていくことも開発を進めるうえで大事だと思いました。

QUOカードPay オンラインストアのリライトはじめました

クオカード デジタルイノベーションラボの齋藤です。 2020年6月にQUOカードPay オンラインストアの内製化を行いましたが、そのタイミングでは修正しきれなかった問題が残っていました。 今後の機能追加や改修を行う上での障害になる為、方針を決めた上でリライトを進める事にしました。

方針

  • 修正することが多いWebのバックエンドから着手する。

  • できるだけ部分的に修正する

Pull Requestが巨大になってしまうとコードレビューが難しくなる為、またリリース時にトラブルが発生するリスクも大きくなるため、できるだけ局所的な改修になるように進めています。

  • 細かい部分に拘りすぎないようにし、まずは全体的に改善する

一旦全体的に厳しい部分を修正したい為、まずは修正内容で記載した内容のみを修正し、それ以外の問題については後回しにすることにしました。

今回導入するライブラリ

以下のライブラリを今回新たに導入しました。

通常は kotest + MockK という組み合わせになると思いますが、Kotest のクラス群でコンストラクターにわたす関数ないしは、 init で書く処理は、関数の見た目とは反してクラスボディにかなり近い性質を持つという特性があります。 そのため、テストを書いているときに値を関数の見た目に騙されて変数等の状態と誤解してテストを書いてしまうケースがあり、今回のようにベンダーから引き継いだ状態のよくわからないコードをリファクタリングしていく際にテストが悪いのかプロダクションコードが悪いのか混乱する可能性が考えられたので、書いている状態を勘違いしづらい JUnit を使った方がよいと判断しました。

  • MockK

Kotlin で作ったクラスでもモックできるというのが特性(Mockito だとプロダクションコードを open にしないといけない)なので採用することが多いですが、 Kotlin で使うようにする目的で作られているので Kotlin の用語にあわせた API になっているというのもポイントであると考えています。 ( Mockitoの場合、動作を記録するのに、 Kotlin の予約後である when という API の使用が絶対で、その場合に

`when`

と書かないといけないため、 Kotlin では使いづらいというのがあります ) ただし、 spring-test の MockBean ではMockitoが使われているので、そのままMockito を使っています。

  • spring-test

優先する修正内容

  • nullをOptionalで書き換える

  • フィールドインジェクションをコンストラクタインジェクションに変更する

  • デッドコードを消す

  • できるだけテストを書く

全てテストを書こうとすると非常に時間がかかってしまう可能性がある為、今回は難しい手動でテストする形も許容する事にしました。

  • 依存を減らす

テスト作成を容易にするため、クラスを分割するなどし依存しているモジュールの数を減らそうとしています。

2巡目以降のリファクタリングで対応する予定のもの

今回はサーバーサイドを優先に進める事にしましたが、以下についても今後対応していこうと思います。

  • フロントエンド

  • バッチ

  • Kotlin化

  • できるだけ参照透過に書き換える

リモートでのユーザビリティテスト実施

こんにちは。

今まで対面で実施していたユーザビリティテストを初めてリモート環境で実施したので、ブログでご紹介します。


実施の背景

現状では対面での実施が難しく、リモートでの実施を検討することとなりました。


実施までの準備

スクリーニングアンケートの作成配信、評価項目、テストタスク、全体スケジュールなどの準備は通常通り進めましたが、操作してもらうUIをどのように用意するかと進行方法については悩みました。弊社はデザインにFigmaを利用しているので、Figmaでプロトタイプを作って操作してもらうのも検討したのですが、被験者の方にアプリをインストールしていただいたり、スマートフォンの環境によってはうまく動作しなかったりといったことが想定され、実施や実施準備への影響が懸念されたので、今回は採用しませんでした。(別のプロトツールですが、過去に在籍した企業でそのような経験もしたので。。。)代わりに画面キャプチャを貼り付けたスライドを用意し、被験者の方に口頭で操作する場所を発話してもらい、こちらで画面を切り替えて擬似的に操作していただくこととしました。普段のユーザビリティテストで行う脳内で考えていることを出来るだけ声に出して操作してもらう延長のイメージです。 調査は準備が重要と言われていますが、今回はリハーサルが本当に重要でした。もし今までと同じ感じでリハーサルを軽くしていたら、、、1人分の結果を無駄にしていた可能性が大きかったです。


実施

Zoomを利用して既存UIと新規UIの評価を上記のとおり紙芝居形式で実施しました。


対面との違いで感じたこと

今回の形式は、実際にご自身で操作できないため、普段よりも考えて操作することになるので、操作に行き詰まった時に色々と探して操作する部分を拾いづらい場合がありました。また、同じ空間に居ないので、感情を感じ取るのが難しかったです。なので、色々と聞き出すために普段よりもこちらが話すことが多かったように感じます。もちろんペーパープロトなので説明なども多いので意外と喉が乾きました。こまめに喉を潤わす準備も忘れずにしたいです。


実施して良かったこと

初めての試みだったので、少し不安を感じていましたが、課題を抽出でき、改善案の検討に繋がった活動になりました。絶賛開発中ですので、お楽しみにしていただけると嬉しいです。また、過去に実施した際は都内で開催していたのですが、今回はお住いの場所に限らず、より幅広くご参加いただけたと思います。 インタビュー調査はすでにリモートで実施ていましたが、ユーザービリティテストもリモート実施の実績ができたので、現在の対面が難しい状況がいつまで続くか分からないですが、生成的調査、検証的調査を状況に応じて使い分けながらプロダクト開発を進めていけるということが確認できました。見学する人数が多くても被験者の方に圧迫感を与えずに済むのもメリットとしてありそうでした。


最後までお付き合いありがとうございました。以上となります。

GitHub Actions の composite run steps action を 試してみた

今回はちょっと前(2020 年 8 月 7 日)に公開された GitHub Actions の composite run steps action を 試してみました。これは Dockerfile や node js のコードを書かなくても(書けなくても) 通常の GitHub Actions のワークフロー YAML が書ければ、 action を作成・公開できるという仕組みです。

続きを読む

デジタルイノベーションラボのチーム構成

今回はデジタルイノベーションラボのチーム構成について書きます。

現在以下の4チーム体制になっています。

  • QUOカードPayチーム(3名)
  • QUOカードPay ECチーム(2名)
  • 新規プロジェクトチーム(5名)
  • 運用チーム(3名)
  • UI/UXチーム(2名)

UI/UXと運用チーム以外はスクラムで進めており、コーディング作業はペアプロ/モブプロを中心に進めています。

QUOカードPayチーム

QUOカードPayのサービスイン前後からいる古株が集まったチームです。 主に以下の開発を行っています。

サービスインに向けて急ぎ作った箇所もあるので、今後の長期の運用に耐えられるよう、新しい技術を取り入れる等して、自動化やブラッシュアップを進めています。 例えば、最近ではCircleCIからGitHub Actionsに完全移行するなどしました。

モバイルアプリで利用している技術 (https://quo-digital.hatenablog.com/entry/2020/01/27/124302)

バックエンドで利用している技術 (https://quo-digital.hatenablog.com/entry/2020/01/20/131409)

QUOカードPay ECチーム

QUOカードPay オンラインストア(https://pay.quocard.jp/)を担当しています。QUOカードPay オンラインストアは最初は外注で構築し内製化をしました。経緯はこちら(https://quo-digital.hatenablog.com/entry/2020/07/03/130141) ゼロから社内で構築したものはKotlinで作っていますが、こちらはJavaで構築したものを徐々にKotlin化しようとしています。また技術的には色々と改善したいものが残っている状態なので、機能追加や仕様変更の対応を行いつつ徐々に改善を進めています。

一般のECサイトとは少し異なり、法人の顧客の方が多く生のフィードバックも得られやすいため、仕事の成果が目に見えて実感できます。 現在、QUOカードPay ECチームはスクラムの開発チームとしては最小とされる3人を下回っているため、一緒に働くメンバーを募集しております。

新規プロジェクトチーム

QUO カード Pay のバックエンドを改善・再構築して QUO カード Pay ビジネスを柔軟かつ迅速に進化するために日々躍進しているチームです。しかし、今のところ大した成果を上げていないので日陰者のような生活をしています。 基本的には他のチームと同様に、Kotlin アプリケーションを Terraformで構築した ECS クラスタ上で動かすようなシステムの構成になる予定です。 これまたほかのチームと同じですが、2 週間のスクラムをチーム全体の仕掛中の作業が複数にならないようにモブプログラミング・モブ作業の形態で取り組んでいます。モブ〜の取り組みについては、以前書いた記事(https://quo-digital.hatenablog.com/entry/2020/09/09/121233) を参照していただけると詳細がわかると思います。

運用チーム

QUOカードPayに関わる様々な運用作業を担当しているチームです。 定期的な作業や非定期な依頼をはじめとして、各部署やQUOカードPayをご利用されている企業様からのお問い合わせや、システムの障害にも対応しています。 運用チームのモットーは「安全・安心・確実」と「運用改善」で、「縁の下の力持ち」を目指して日夜奮闘中です。 突発的な業務が多く、現在はカンバンのような形で進めていますが、将来的には各スクラムチームに合流する予定です。

UI/UXチーム

QUOカードPayのユーザー体験の改善をするために、ユーザーへの調査を実施し、調査より浮かんできた問題点や改善点を明確化し、より良い体験へアプローチするデザインを作りエンジニアと二人三脚でサービスに落とし込んでいくチームです。 ユーザーの声を幅広く知るため、コールセンターとも連携し、お客様の声を汲み取り、より良いサービスデザインを目指します。 また、企業のプロモーション企画にQUOカードPayを採用してもらうための企業調査や、マーケティングチームとの連携もしていきます。

クオカードでは現在エンジニアを募集しています。

quo-digital.jp