QUO CARD Digital Innovation Lab Tech Blog

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

OSSへの支援について

クオカードでは様々なOSSを利用していますが、現在は業務として技術的な貢献をする余裕が無い為、寄付という形で支援をすることにしました。

今回は社内の多くのプロジェクトで利用しているJUnit 5に寄付をしました。

https://junit.org/junit5/

今後もJUnit 5に限らず、できるだけ継続的に多くのOSSの支援をしていきたいと考えています。

クオカードではソフトウェアエンジニアを募集しています。

https://quo-digital.jp/

研修体験記:Spring Core(社外)

あけましておめでどうございます、三度のJavaよりメシが好きなデジタルイノベーションラボの山本です。

2022年最初の記事を投稿させていただきます。

年明けにプロダクトグループメンバー数名でSpringFWのハンズオン研修を受講したので今日はそれについて書きたいと思います。

私は、10年以上エンジニア(主にJava)としてシステム開発に携わっており、SpringBootはここ数年よく使うFWとなっております。 基本的なSpringの知識は業務を通じて、中でもわからなかったものに関しては別途本やサイトで調べて補完してきました。 本と言っても、最低限必要そうなところを読むといったスタイルで最初から最後まで通して読んだ本はありませんでした。

弊社でも多くのプロダクトのコア技術としてSpringBootを採用しており、 それぞれバックグラウンドの異なるエンジニアのSpringの知識の平準化を図るべく、社外のハンズオン研修の受講が去年計画されました。 それが実現した、今回受講した研修はこちらとなります。

VMware Tanzu認定 Spring Core: Training https://www.casareal.co.jp/ls/service/openseminar/vmware/v013

研修は、四日間で9:30 - 17:30(昼休憩1時間)で行われました。

1日目:Spring Frameworkの基礎

2日目:アスペクト指向プログラミングに関して

3日目:SpringBoot(MVCトランザクション管理など)

4日目:SpringBoot(REST API作成、Test, SpringSecurityなど)

1〜2日目は、メニュー通りSpringFrameworkやAOPの基本に関して取り扱い、古い記憶をリファインメントしつつ、もちろん中には初めて知ることもある内容でした。 残りの日は、SpringBootを中心に取り扱い、今まさに業務で使用している内容でした。 中でも個人的にはテストケースを書くたためにSpringが提供してくれる機能を一部しか使っていないことを知ることができたのが今回の研修での一番の収穫でした。

私の今までの経験上、とりわけテストケース作成は最初に作成した人のものをベースに右へ倣えで作成していくパターンが多く、 テストケースの書き方を深堀りした記憶も改善しようと考えたこともなく、いかにその状況に「漬かって」いたかを実感しました。

四日間という期間は少し長かったですが、新年のスタートを切るには良い研修経験となりました。 また機会があれば、別の研修にも参加したいと思います。

それでは、メシ行ってきます。

OpenAPI を利用した開発フロー

こんにちは、デジタルイノベーションラボの飯島です。 今回は、OpenAPI を利用した開発とデプロイまでのフローをざっくり紹介します。

アプリケーション構成

バックエンドは Kotlin + SpringBoot, フロントエンドは TypeScript + React になります。 バックエンドが用意した API をフロントエンドが叩く、一般的なアプリケーションです。

フロー

1. OpenAPI 定義出力用の Interface を実装したコントローラーを作成

簡単なサンプルですが、以下のような Interface を作成し、implement してコントローラーを作成します。

@Tag(name = "UserCreate", description = "ユーザーの登録")
interface UserCreateController {

  @Operation(summary = "ユーザー登録")
  @ApiResponses(
      ApiResponse(responseCode = "200", description = "登録成功"),
  )
  fun create(user: User): HttpResponse
}

data class User(val name: String)

実装クラスに直接アノテーションを記載しても良いのですが、API 定義と実装が混在すると可読性が下がるため、Interface を分けています。

アノテーションspringdoc-openapiで提供されているものを使っています。

2. Interface から OpenAPI 定義の yaml ファイルを生成する

openapi-generator gradle plugin を使用し、Interface から OpenAPI 定義の yaml ファイルを生成します。

3. 生成された yaml ファイルから、フロントエンド用のコードを生成する

OpenAPI Generator を利用して、バックエンドの API を呼び出すための TypeScript コードを生成します。 生成されたコードの内部で使用される Http client については、axios や fetch, ajax など複数の中から選択できます。 docker での生成にも対応しているので、バックエンド側で生成した openapi.yml を引数に generator を実行します。

4. 生成された API 呼び出しコードを利用してフロントエンドをコーディングする

React で画面を作り、3 で生成されたコードを使用してバックエンドとやりとりするようにします。 以下のコードでは、UserCreateApi, createConfiguration, ServerConfiguration がそれぞれ自動生成されたコードになります。 先ほどバックエンドの Interface に定義した create メソッドが UserCreateApi に用意されています。

  const api = new UserCreateApi(
    createConfiguration({
      baseServer: new ServerConfiguration(url, {}),
    })
  )

  api.create({
    name: "taro"
  })

生成されたコードも TypeScript なので、型安全に使用することができます。

5. Slack からデプロイする

弊社ではほぼ全てのプロジェクトが Slack からデプロイできるようになっています。 動作確認などで検証環境にデプロイする用にデプロイマン、本番環境にリリースする用にリリースマンという bot を作ってあるので、コマンド一つで GitHub から簡単にデプロイすることができます。

最後に

バックエンドの API 定義箇所とフロントエンドの呼び出し箇所は、受け渡すデータは同じなのに言語が違うことで書き方が変わってくるため、こうして自動化できるとコーディングがかなり楽になります。 open-api generator の具体的な使い方などはまた別の機会に記事にしたいと思います。

チームのコミュニケーションについて

クオカード デジタルイノベーションラボの齋藤です。

今回はチーム内のコミュニケーションについて説明します。

MTG

デジタルイノベーションラボではCOVID‑19の影響で2020年の春頃からフルリモートになっています。 現在はスクラムで進めており、毎日チーム毎にデイリースクラムを実施しています。

Google Meet、Slackのhuddleなどチームによって使うツールは異なっていますが、MTGは全てオンラインで行っています。たまに他部署とMTGを行う事がありますが、そちらも基本的に全てオンラインになっています。

またスプリント毎のスプリントレトロスペクティブの他に、月1でチーム全体での振り返りを行っています。

Slack

デジタルイノベーションラボではできるだけチームで協力して業務を進める形にしようとしており、割と頻繁にコミュニケーションを取っている方ではないかと思います。

仕様の認識合わせや実装中に困っている事などはまずSlackで議論し、込み入った話になってきたらhuddleで話し合うという進め方になっています。

スプリント中のやりとり

フルリモートになる前からできるだけ自分が何をしているか、確認されなくても周囲の人に伝わるようにしようとしていましたが、リモート中心になってからよりそこを意識するようにしています。

周囲が何をしているかわからないと、リリース直前になって問題に気づいてリリース延期になったり、もしくは周囲からのヘルプを得られず嵌ってしまうなどの問題が起きやすいと考えています。

具体的にはGitのpushをまめに行う、Jiraのチケットにまめにコメントする、またプログラムが完成してからドキュメントを書くのではなく、実装中にWIPという形でドキュメントを公開し、随時追記・修正していく等の進め方にしています。

また悩んでいる事やこれからやろうとしている事をSlackのTimesでコメントしたりしています。

このような対応を行う事により、もし進め方や進めている作業に問題があった場合は早めに周囲の人からコメントをもらえたり、困っている内容について知見のあるメンバーからヘルプを受けたりできるようにしています。

Zoom飲み

上記のように、チーム内のコミュニケーションはそれなりにうまくいくようになってきたと思います。ただ他チームのメンバーとやりとりする機会がそれほど多く無い為、新しく入社したメンバーが他チームにどういう人がいるかわからない、また馴染むのに時間がかかるという問題が起きています。

その為希望者でZoom飲みを実施してみましたが、何の準備もしていなかった為か緊張感のある飲み会になってしまいました。

何かやることを決めた方が話しやすくなりそうというアイデアが出たのでゲームをやってみたところ盛り上がったので、今後も色々と試行錯誤していこうと思います。

開発マシンをM1 MaxのMacBook Proに切り替えることにしました

クオカード デジタルイノベーションラボの齋藤です。

2021/10/19 にM1 Maxを搭載した新型MacBook Proが発表されたので、エンジニア全員の開発マシンを切り替える事にしました。

今まではメモリを32Gにする為にMacBook Pro 15インチもしくはMacBook Pro 16インチを使っていましたが、基本的に14インチにすることにしました。

以下切り替える事にした理由です。

・現在使っている開発マシンの償却は終わっておらず、問題なく開発できているという声もあったものの、ビルド時間短縮など、早めに切り替えた方がROIが高いと判断し切り替える事にしました。デジタルイノベーションラボでは極力マネージドサービスを利用するなど、コストを払えば効率化できる部分はそのように進めています。

・COVID‑19の影響で全員ほぼフルリモートワークの状況になっていますが、所属しているチーム外の人と話す機会がなくなった、また質問がしにくい等で、COVID‑19の感染状況が落ち着いたら出社する日があっても良いのではという声がメンバーから上がっていました。16インチは2キロ以上と持ち歩くのは少し重い為、また家では外付けモニタを使っている人が多い為、基本的に14インチにすることにしました。

ちなみに個人的には新型Mac miniを渇望していたのですが、今回発表されなかったので現行モデルを購入することにしました。

クオ・カード ペイ オンラインストアをリファクタリングしました

こんにちは、日本のテトリスプレイヤーの上位 0.5% に入るデジタルイノベーションラボの飯島です。 今回は Java のプログラムをリファクタリングした話を書きます。

背景

2020年6月にクオ・カード ペイ オンラインストアの内製化を行いました。 その時は EC2 で動いていた Java プログラムを ECS に乗せるために必要な修正を加えましたが、ソースコードの大半には手を加えていませんでした。 内製化から1年と少し経ちますが、バグの修正や新規機能追加の際に複雑なソースコードの解読に時間を取られることが多く、これはまずいということで2ヶ月間他の機能追加無しでリファクタリングをすることにしました。

方針

プロダクトの全体的な設計から見直すのが理想でしたが、そこまでやると果てしない時間がかかってしまうため、少ない時間で効果が得られそうな箇所や、今後よく触りそうな箇所を重点的にリファクタリングするようにしました。

実施した内容

Kotlin 化

QUOカードではサーバーサイド実装の言語としてKotlinを採用しています。 今回リファクタリング対象となるコードはJavaで書かれていたため、まずKotlin化をすることにしました。 Kotlin化により nullとmutableの扱いが限定されるので、可読性の向上とバグの減少が見込まれます。

null まわりの修正

元々のソースコードでは @Nullable や @NotNull が使われていなかったため、引数に null が入ってくるのか、戻り値として null が考えられるのかがぱっと見でわからないようになっていました。 そのため不要な null チェックが蔓延していたり、また必要な箇所に null チェックが足りなかったりという状態でした。

Kotlin では基本的に null は許容されず、型の後に ? をつけることで nullable になります。

val a: String? = null
val b: String = null // コンパイルエラー

Java から Kotlin に変換する際には型に ? をつけておき、可能なら ? を削除するという方針でリファクタリングしました。 ただ、この修正は当初考えていたよりもずっと難しく、まだかなりの量の ? が残ってしまっています。 理由としては、元々のソースコードを読んでも使われている値が null を許容するのかしないのかの判断が難しかった為です。 例えばメソッドの引数で言うと、そのメソッドを呼んでいるメソッド、そのメソッドの引数がそのまま使われているなら更にその呼び出し元、というように追いかけて虱潰しに調べないといけません。 クラスのフィールドで言えば、それが使われている全ての箇所を調べないと判断ができません。

変数の不変化

Kotlin では、val で宣言した変数は不変 (immutable) になり、後から変数に値を代入しようとすることができません。これにより、一度初期化した変数がソースコード中のどこかで変更されないことがコンパイラレベルで保証されるので、変数に何が入っているのかを把握するためにソースコードを深く潜っていかなくてもよくなります。(詳しくは「副作用」の箇所に書きます) リファクタリングにあたり、ローカル変数でもクラスのフィールドでもとにかく可能な限り val で宣言することを意識しました。

綴りミス(タイポ)を修正

当たり前ですが綴りミスは無い方が良いです。 コードリーディングの妨げになりますし、正しい綴りで検索をかけた時に引っかからなくなります。 例えば、Downloadで検索したのに、Donloadと綴りミスしていたら一生引っかかりません。 (DonloadはもしかしたらDonald なのでは?と社内で議論になりました) 今回は一つのクラス内に2, 3個綴りミスがあるというレベルだったので、他の項目のリファクタリング中に直せる箇所だけ直す、という対応をとりました。

修正時の注意点

thymeleafなど実行時に名前を解決する処理がある場合、Java側の綴りミスを直してコンパイルが通っても実行時にエラーが発生します。 この部分のリファクタリングは影響範囲の調査に時間がかかるため、今回は後回しにしています。

String の配列をクラスに変更

例えば名前と年齢を持つ犬を表現したい時、一般的にはクラスを作ると思います。

data class Dog(val name: String, val age: Int)

これをStringの配列で表している箇所がありました。

// 0番目の要素に名前、1番目の要素に年齢を入れる
String[] dog;

年齢を取り出す場合は Integer.parseInt(dog[1]) となっています。 さらに、犬をたくさん扱う場合はこうなっていました。

String[][] dogs;

実際にはdogsのような体を表す変数名はつけられておらず、result のような名前だったため、変数宣言から何を意味するのか読み取れず苦戦しました。 こちらは Dog クラスを用意することで対応しました。

フラグ保持用の Map<String, String> を List に修正

飼い主に対して複数の猫が紐づくモデルを考えます。 単純に DB から猫を取ってくると、このようなクラスにマッピングできます。

class Cat {
    private Person person; // 飼い主
    private String name;
}

DB から取ってきた List に対してループを回す中で、既に登場した飼い主を保持しておく Map を作っている箇所がありました。

Map<Person, String> map;
for (Cat cat: cats) {
    if (!map.get(cat.person).equals(“”)) {
        // キーが存在しない場合の処理
    } else {
        // キーが存在する場合の処理
        map.put(cat.person, “”);
    }
}

最初はこの map が何をしているのかを理解するのに時間がかかりましたが、単純に処理済みの飼い主を保持しているだけということがわかりました。

val processedPersons: List<Person>;

としてリストに持たせて変数名を分かりやすくすることで解決しました。

private メソッドを別クラスの public メソッドにする

他のクラスが持つべきメソッドが private メソッドとしてコントローラーやサービスに実装されていたので、そういうものはあるべき場所に実装を移動しました。

private String getDogInfo(Dog dog) {
    return dog.getName() + “ ” + dog.getAge().toString();
}

⬇️

data class Dog (val name: String, val age: Int) {
    val info: String
        get() = “$name $age”
}

String クラスや各種ライブラリが提供するクラスなど、自分で修正することができない型に対するメソッドに関しては、拡張関数を作成して対応しました。 型をラップしたクラスを作って必要なメソッドのみを生やすという方法も考えられますが、今回は手間も考慮して拡張関数方式にしました。 拡張関数を使うと、メソッドのネストをチェーンで記述できるようになるので、単純な可読性も向上しました。

private String addExclamationMark(String s) {
    return s + “!”;
}

public void a() {
    addExclamationMark(
        addExclamationMark(
            addExclamationMark(“hello”))) 
}

⬇︎

fun String.addExclamationMark(): String = “$this!”

fun a() {
    “hello”
        .addExclamationMark()
        .addExclamationMark()
        .addExclamationMark()
}

メソッドから副作用を取り除く

副作用のあるメソッドはコードリーディングの妨げになります。

public class Fox {
    private String name;
    private int age;

    // setter & getter
}

public void a() {
    Fox fox = new Fox();

    setName(fox, “taro”);
    fox.setAge(3);

    System.out.println(fox);
}

private void setName(Fox fox, String name) {
    fox.setName(name + “-chan”);
}

このコードでは、最初に宣言された時の fox と println した時の fox では状態が異なります。println 時点での fox の状態を知るには、fox が宣言された場所から println される場所までの全ての行を読まないといけません。もし fox が宣言された時点から変更されないことが保障されるなら、println がどれだけ離れていても fox の状態はすぐ把握できます(そもそも変数の宣言と使用箇所が離れていること自体がよくないですが)。

data class Fox (val name: String, val age: String)

fun a() {
    val fox = Fox(getName(“taro”), 3)
    println(fox)
}

private fun getName(name: String): String = “$name-chan”

Kotlin だとこのように書けば、宣言時から変更されることはありません。

副作用を持つメソッドの見分け方として、まず戻り値のない(voidの)メソッドは怪しいです。setXXX という名前のメソッドもほぼ副作用があります(中には BigDecimal のように新しいオブジェクトを返すものもありますが)。あとはリファクタリング中のメソッドから呼び出しているメソッドの中を逐一確認していきます。副作用を持つメソッドを呼び出しているメソッドも副作用があることになるので、呼び出し階層を全て辿る必要があります。

修正方法ですが、副作用としてセットする値を返すようにします。上の例でいうと、setName の中で fox に値をセットしていたところを、getName として値を返し、呼び出し元の方で使用するようにしています。 何重にもネストしたプライベートメソッドの中に副作用がある場合は、こうやって少しずつ副作用を上のメソッドに押し出していき、最後に呼び出し元で使用するようにします。 副作用の無いメソッドが完成したら、副作用のある元のメソッドは Deprecated でマークし、「副作用があるため、極力 XXX を使用すること」とコメントしておきます。 こうすると Intellij などの IDE では呼び出し箇所に取り消し線が引かれるため、今後リファクタリングするときの目印となります。

終わりに

リファクタリングをすればするほど「これは一から書き直した方が早いのでは。。」という気持ちに見舞われましたが、実際はソースコードの奥深くに隠された仕様を発見するための時間が必要になるので、やっぱり時間が取れたタイミングでリファクタリングしていくのが現実的かな、とも思いました。

また、技術的負債は放っておくと返済にかかるコストが上がっていくと言うのを身を以て体験しました。クリーンアーキテクチャの最初の方に、「時間がある時に綺麗にすればいいと思うかもしれないが、そんな時間は一生来ない」のようなことが書いてありましたが、自分も実装するときはそれを意識しようと思いました。今回は何をするにもソースコードの解読に多大なコストがかかってしまう状態になっていたのでリファクタリングの時間を設けざるを得ませんでしたが、最初から綺麗なコードだったらもっと生産的なことに時間を割けたのに、、と考えると、常にその時点で最も読みやすいコードを書く努力は大事だと思います。(ただ、リファクタリングしてコードを綺麗にしていく作業自体は楽しかったです。)

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は時差情報を持っていないので使うときは気をつけましょうという話でした。 それでは楽しい時差ライフを!!