pre-commitを使用してgit commit時にPrettierを実行させる

こんにちは。エキサイトでデザイナーをしている齋藤です。

前回、【JetBrains製エディタ対応】PrettierでTailwind CSSのclass名を規則的に自動ソーティングさせる と称して、IntelliJ IDEAでファイル保存時にPrettierを自動実行させる方法をご紹介しました。

前回の記事でご紹介した方法は、IntelliJ IDEAやVS Code特有の環境設定を用いたものでしたが、pre-commitを使用することでエディタに依存せずにPrettierを自動実行できることを業務の中で学びましたので共有します。

pre-commitとは

pre-commitとはgit commit時に定義されたシェルスクリプトなどを自動的に実行させることができるツールです。

今回は、シェルスクリプトにPrettierを実行させるスクリプトを含めて、git commitに自動的にコードフォーマットがされるようにします。

導入方法

開発環境

Spring Boot(v2.6.2)+ Thymeleafなプロジェクトです。PCはM1 Mac ProMac OS Monterey)を使用しています。

プロジェクトのディレクトリ構成

(root)
├── web
│   ├── app(Webアプリケーション本体が格納されている)
│   │    ├── src
│   │    ├── package.json
│   │    └── ...
│   └── ...
└── ...

インストール済みのnpmパッケージ

├── prettier-plugin-tailwindcss@0.6.1
├── prettier@3.3.1
└── tailwindcss@3.4.3

PCにpre-commitをインストールする

まずは、Homebrewを使用してpre-commitをインストールします。

brew install pre-commit

インストールが完了して pre-commit --versionを叩き、以下のようにバージョンが出力されれば成功です。

pre-commit 3.7.1

プロジェクトにpre-commit用のシェルスクリプトを格納するディレクトリを作成する

(root)
├── web
│   ├── app
│   │    ├── src
│   │    ├── package.json
│   │    └── ...
│   └── ...
├── git-hooks <- 追加
└── ...

パスを通す

pre-commitシェルスクリプト実行できるように、先程追加した/git-hookspre-commit用のシェルスクリプトを格納したディレクトリであることを設定します。

git config --local core.hooksPath ./git-hooks

シェルスクリプトを書く

/git-hooksシェルスクリプト用のファイルとしてpre-commitを作成し、スクリプトを記述します。

├── web
│   ├── app
│   │    ├── src
│   │    ├── package.json
│   │    └── ...
│   └── ...
├── git-hooks
│   └── pre-commit <- 追加
└── ...

今回はpackage.jsonに定義したPrettier用のnpmスクリプトであるnpm run prettier:formatを実行させるスクリプトの例をご紹介します。

まずは完成形です。

#!/bin/bash

# 現在いるディレクトリを取得
initialDir=$(pwd)

# フォーマット前の変更ファイルを取得
beforeFileList=$(git diff --name-only --cached --diff-filter=d)

# Prettierを実行させるnpmスクリプトを実行
echo "Prettier format start." && cd web/app && npm run prettier:format && cd "$initialDir" && echo "Prettier format complete."

# フォーマット後の変更ファイルを取得
afterFileList="$(git diff --name-only --cached --diff-filter=d)"

# フォーマット前と後で差分がなければgit addを実行して、差分があればエラーを出現させる
if [ "$beforeFileList" = "$afterFileList" ];
then
   for file in $beforeFileList
   do
     git add $file
   done
else
   echo "Non-change files have been formatted. Please check."
   exit 1
fi

細かく説明していきます。

Bashシェルでの実行宣言

スクリプトがどのシェルで実行されるべきかを指定します。今回はBashシェルを指定します。

#!/bin/bash

現在いるディレクトリを取得

今回はnpmスクリプトを実行させるため、package.jsonが存在するディレクトリで実行させる必要があるため、ディレクトリ移動を行います。実行後に戻ってこられるように、予め、現在いるディレクトリを取得しておきます。

initialDir=$(pwd)

フォーマット前の変更ファイルを取得

ステージされている変更のうち、削除されたファイルを除いたすべての変更されたファイルの名前がリストを取得しておきます。

  • git diff : Gitリポジトリ内の変更点を表示するコマンドです
  • --name-only : 変更されたファイル名だけを表示するオプションです。変更の詳細(具体的な行の変更など)は表示されません
  • --cached : インデックスにステージされた変更だけを対象にします。つまり、git addされたけれどまだコミットされていない変更が対象です
  • --diff-filter=d : 「削除されたファイル」の変更を除外します
beforeFileList=$(git diff --name-only --cached --diff-filter=d)

Prettierを実行させるnpmスクリプトを実行

メッセージを添えて、Prettierを実行させるnpmスクリプトを記載します。

その際、package.jsonが存在するディレクトリでないとnpmスクリプトが実行できませんので、ディレクトリ移動をしたのちにnpmスクリプトを実行し、その後、予め取得しておいた現在のディレクトリに戻るようにします。

echo "Prettier format start." && cd web/app && npm run prettier:format && cd "$initialDir" && echo "Prettier format complete."

フォーマット後の変更ファイルを取得

フォーマット後の変更ファイルを取得します。

afterFileList="$(git diff --name-only --cached --diff-filter=d)"

フォーマット前と後で差分がなければgit addを実行して、差分があればエラーを出現させる

フォーマット前後のファイルリストが一致するかどうかをチェックします。一致する場合は、各ファイルをステージングします。

一致しない場合は、Prettierによるコードフォーマット以外の影響で変更が生じたことになりますので、エラーを出力してスクリプトを終了させます。

if [ "$beforeFileList" = "$afterFileList" ];
then
   for file in $beforeFileList
   do
     git add $file
   done
else
   echo "Non-change files have been formatted. Please check."
   exit 1
fi

エラーが発生してシェルスクリプトが実行できない場合

シェルスクリプトを定義してgit commitしても以下のようなエラーが発生して実行できないことがあります。

--- (エラー文) ---
hint: The 'git-hooks/pre-commit' hook was ignored because it's not set as executable.
hint: You can disable this warning with `git config advice.ignoredHook false`.

これはpre-commitに実行権限がないために発生します。

その場合は、/git-hooksに移動して実行権限を付与してください。

chmod +x pre-commit

さいごに

今回は、pre-commitを使用してgit commit時にPrettierを実行させる方法をご紹介しました。

これにより、エディタに依存しない方法でPrettierが自動実行できるようになるため、開発メンバーが個別にエディタの環境設定を変更する手間も省くことができました。

ご精読ありがとうございました。

【Terraform】 GitHub Actions から OIDC 認証によって AWS ECS にデプロイ

エキサイト株式会社の@mthiroshiです。

GitHub Actions から AWS ECS にコンテナアプリケーションをデプロイする際に、OIDC 認証を使用して AWS にアクセスする方法を試してみました。その内容について紹介します。

GitHub 公式ドキュメントは下記です。 docs.github.com

OpenID Connect(OIDC)とは

OpenID Connect(OIDC)は、OAuth 2.0 フレームワーク仕様に基づいた認証プロトコルです。 OIDC を利用することで、アプリケーションはユーザーを検証し、必要なユーザー情報を取得できます。

GitHub Actions から AWS ECS へデプロイする場合、AWS へのアクセスには認証が必要となります。 認証方法の一つとして、IAM ユーザーのアクセスキーとシークレットキーを用いる方法がありましたが、この方法はキーのセキュアな管理が必要であり、運用上の課題となっていました。

それに対して OIDC を用いる場合では、有効期間の短いクレデンシャルを AWS から直接要求するようにワークフローを構成できます。 OIDC では、アクセストークンと呼ばれる有効期限付きのトークンを発行するため、アクセスキーやシークレットキーのように永続的に管理する必要がなく、セキュリティリスクを低減することができます。

AWS IAM の ID プロバイダーに GtiHub OIDC プロバイダーを追加

GitHub の OIDC プロバイダー を AWS IAM の ID プロバイダに追加します。これによって、GitHubAWS アカウント間の信頼を確立します。

Terraform コードを紹介します。

data "tls_certificate" "github_actions_deploy" {
  url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
}

resource "aws_iam_openid_connect_provider" "github_actions_deploy" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = [data.tls_certificate.github_actions_deploy.certificates[0].sha1_fingerprint]
}

aws_iam_openid_connect_provider リソースによって、IAM OIDC ID プロバイダーを作成できます。

下記ページを参考にして、パラメータの値を当てはめています。 docs.github.com

For the provider URL: Use https://token.actions.githubusercontent.com

For the "Audience": Use sts.amazonaws.com if you are using the official action.

thumbprint_list には、 OIDC ID プロバイダーのサーバー証明書サーバー証明書のサムプリントが求められるので、 tls_certificate data リソースによって取得し、設定します。

GitHub Actions 用の IAM ロールを作成

GitHub Actions から AWS にアクセスするための IAM ロールを作成します。 IAM ロールを作成し、下記の3種類の IAM ポリシーをアタッチします。

  • OIDC 認証用の信頼ポリシー
  • ECS デプロイ用の許可ポリシー(アプリケーション共通)
  • ECS デプロイ用の許可ポリシー(アプリケーション個別)

まず、 IAM ロール作成と、OIDC 認証の 信頼 ポリシー、ECS デプロイ用の許可ポリシー(アプリケーション共通)のアタッチについて、Terraform コードを示します。

data "aws_iam_policy_document" "github_actions_deploy_assume_policy" {
  statement {
    principals {
      type = "Federated"
      identifiers = [
        aws_iam_openid_connect_provider.github_actions_deploy.arn
      ]
    }

    actions = [
      "sts:AssumeRoleWithWebIdentity"
    ]

    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values   = ["repo:<GitHubオーガニゼーション>/<GitHubリポジトリ名>:*"]
    }
  }
}

resource "aws_iam_role" "github_actions_deploy" {
  name               = "github-actions-deploy"
  description        = "IAM Role for GitHub Actions Deploy ECS"
  assume_role_policy = data.aws_iam_policy_document.github_actions_deploy_assume_policy.json
}

data "aws_caller_identity" "self" {}

data "aws_iam_policy_document" "github_actions_deploy_policy" {
  statement {
    sid = "GetAuthorizationToken"
    actions = [
      "ecr:GetAuthorizationToken"
    ]
    resources = ["*"]
  }

  statement {
    sid = "RegisterTaskDefinition"
    actions = [
      "ecs:RegisterTaskDefinition"
    ]
    resources = ["*"]
  }

  statement {
    sid = "PassRole"
    actions = [
      "iam:PassRole"
    ]
    resources = [
      "arn:aws:iam::${data.aws_caller_identity.self.account_id}:role/ecsTaskExecutionRole"
    ]
    condition {
      test     = "StringLike"
      variable = "iam:PassedToService"
      values   = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_iam_policy" "github_actions_deploy" {
  name   = "github-actions-deploy"
  policy = data.aws_iam_policy_document.github_actions_deploy_policy.json
}

resource "aws_iam_role_policy_attachment" "role_policy_attachment" {
  role       = aws_iam_role.github_actions_deploy.name
  policy_arn = aws_iam_policy.github_actions_deploy.arn
}

data "aws_iam_policy_document" "github_actions_deploy_assume_policy" では、先ほどの OIDC IDプロバイダーを指定し、AssumeRoleWithWebIdentity アクションによって、一時的な認証情報を取得できるように信頼ポリシーを作成しています。 認証の条件に、 GitHub リポジトリの指定もできます。

そして、data "aws_iam_policy_document" "github_actions_deploy_policy" では、デプロイ用の許可ポリシー(アプリケーション共通)を設定しています。ECRのログイン、ECSのタスク定義登録などのポリシーです。

タスク実行ロール( ecsTaskExecutionRole ) に iam:PassRole をしています。タスク実行の際に、OIDC 認証のポリシーが必要となるためです。

次に、ECS デプロイ用の許可ポリシー(アプリケーション個別)の設定です。

data "aws_iam_policy_document" "github_actions_deploy" {
  statement {
    sid = "PushImageOnly"
    actions = [
      "ecr:BatchCheckLayerAvailability",
      "ecr:InitiateLayerUpload",
      "ecr:UploadLayerPart",
      "ecr:CompleteLayerUpload",
      "ecr:PutImage"
    ]
    resources = [
      "arn:aws:ecr:ap-northeast-1:<AWSアカウントID>:repository/<リポジトリ名>",
    ]
  }

  statement {
    sid = "UpdateService"
    actions = [
      "ecs:UpdateServicePrimaryTaskSet",
      "ecs:DescribeServices",
      "ecs:UpdateService"
    ]
    resources = [
      "arn:aws:ecs:ap-northeast-1:<AWSアカウントID>:service/<サービス名>"
    ]
  }

  statement {
    sid = "PassRole"
    actions = [
      "iam:PassRole"
    ]
    resources = [
      "arn:aws:iam::<AWSアカウントID>:role/<タスクロール>"
    ]
  }
}

resource "aws_iam_policy" "github_actions_deploy" {
  name   = "github-actions-deploy-policy"
  policy = data.aws_iam_policy_document.github_actions_deploy.json
}

resource "aws_iam_role_policy_attachment" "role_policy_attachment" {
  role       = "arn:aws:iam::<AWSアカウントID>:role/<タスクロール>",
  policy_arn = aws_iam_policy.github_actions_deploy.arn
}

ecr:PutImageecs:UpdateSerivice などのアクションでは、それを実行できるアプリケーションを制限しようと考えました。そのように設定するためには、ECS サービスのarnを動的に参照する必要があったため、Terraformを分ける運用としました。

ECS タスクロールにも OIDC 認証を渡せるように iam:PassRole を設定します。

GitHub Actions の設定

ECS にコンテナをデプロイする GitHub Actions の yaml のサンプルコードを示します。

簡単に、チェックアウトから、ECRログイン、ビルド、タスク定義のデプロイまでを示しています。

name: Deploy ECS

on:
  workflow_dispatch:

permissions:  
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<AWSアカウントID>:role/github-actions-deploy
          aws-region: ap-northeast-1

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
     
      # ビルド 省略

      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: task-definition.json
          cluster: "sample_ecs_cluster"
          service: "sample_ecs_service"
          wait-for-service-stability: true

今回の趣旨のポイントを説明します。

まず、 permissionsid-token: write が必要になります。これによって、GitHubの OIDC プロバイダーから JWT を要求できます。

そして、aws-actions/configure-aws-credentials@v4 action の role-to-assume に作成した IAM ロールの ARN を指定します。これで AWS へアクセスが可能になり、以降のECR ログイン等の処理が実行できます。

最後に

GitHub Actions から OIDC 認証によって AWS へアクセスする方法について、まとめてみました。 IAM ロール、ポリシー周りは、理解が浅くてハマった部分でした。ここは改めて整理しておきたいと思います。 参考になれば幸いです。

採用情報

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアル面談もぜひ!) www.wantedly.com

参考記事

アマゾン ウェブ サービスでの OpenID Connect の構成 - GitHub Enterprise Cloud Docs

GitHub ActionsでOIDCによるAWS認証をTerraformで実装する

GitHub ActionsからECSとECRへのCI/CDを最小権限で実行したい | DevelopersIO

チームメンバーとふりかえり会を実施してみた

こんにちは!エキサイト株式会社のまさきちです。

今回は自分の所属するチームで開発プロジェクト終了後にふりかえり会を実施してみた時のことについてお話しします。 メンバーが集まってふりかえりの時間を取ることはあまりなかったので、学びが多い時間でした!

ふりかえり会実施のキッカケ

プロジェクト進行中、または終わった後に改善点や良かった点が出てきてもその場の認識だけで終わってしまう事も多々あります。 時間が経つと出来事を忘れてしまい同じ失敗を繰り返してしまう事も... せっかくの学びを活かせていなくて勿体無いなと思い始めました。

ふりかえり手法について

世の中にはさまざまな振り返り手法やツールがありますが、今回は下記で行いました。

ツール:Miro

 Miroとはオンラインでチームで画面を共有してボードや付箋を使いながら作業ができるオンラインワークスペースです。

手法:KPT

 KPTとはふりかえりのフレームワークで、KEEP(継続すること)、PROBLEM(問題になっていること)、TRY(次回以降に挑戦する事)に分類してディスカッションする手法です。

ふりかえりワークについて

Miroで振り返り用のボードを作成して付箋を使って1時間で行いました。

ワークは下記のステップで実施しました。

1. ふりかえりをどうやるか

 アイスブレイクとして、ふりかえり会でやりたい事やどんな雰囲気がいいかなどを付箋に書いてメンバー同士で共有し合いました。

2.できごとの思い出し

 今回のプロジェクトで起きた事を思い出しながら、良い事も改善点も自由に付箋に書き出しました。

3.KPTに分類して内容を共有したり、カイゼンするためのアイデアを考える

 2で書き出した付箋をKPTに分類して、それぞれの項目に対してディスカッションしました。

4.ネクストアクションを一人ひとつ決めて実施宣言する

 ふりかえって終わりではなくて、次に自分たちが行う改善活動をネクストアクションとして一人ひとつずつ宣言し合いました。

5.ふりかえりのふりかえり

 今回のふりかえり会に対して、GoodとMoreを書き出してふりかえり会のふりかえりを行いました。


ふりかえりワークで出た具体的なKPTの一部をご紹介します。

Keep
  • スケジュール通りに遅れずにリリース
  • 独自の仕様に柔軟に対応できた
  • 質疑やTODOをスプレッドシートで管理することにより、取りこぼしを防げた
  • 定期的に朝会を行ったことで認識のズレがほぼなかった
  • 朝会後に議事録をまとめてSlackチャンネルに投稿したことで内容が整理できた
  • 既存の実装が良かったのでスムーズに進める事ができた
Problem
  • 複雑な仕様の部分で見落としがあった
  • 通知文言の準備に時間がかかった
  • 初期段階での仕様の共有が足りなかった
  • 企画側の仕様書が残せていない
  • 予想していたよりも改修の量が多くて工数がかかった
  • 仕様の理解が不足していて確認する事が多かった
Try
  • ドキュメントを作る時間をスケジュールに組み込む
  • 細かいUIについてはSlackで文字ベースの共有よりも打ち合わせで確認する
  • ハマりそうなポイントを事前にヒアリングする
  • よく使う文言の一覧表を作り、表記揺れを防ぐ
  • 初期段階で企画、デザイナー、エンジニアが集まって仕様について確認する時間を取る

ふりかえりのふりかえり

GOOD
  • プロジェクト中の良かったことがKEEPとして明確になり、次回以降にも生かされる
  • 問題点が可視化されるので、具体的な対策を考えアクションを起こすことができる
  • チームメンバーのフィードバックにより、客観的にふりかえることができる
  • 自分の起こした行動に対して感謝してもらえてモチベ向上に繋がる
MORE
  • 時間が足りない...もっと具体的に話し合いたい
  • 今回はKPTだが、他の振り返り方法も試してみたい

まとめ

ふりかえり会を行うことで行動に対してフィードバックを貰えたのと、改善点についての具体的なアクションが決められたのでやって良かったと思いました。 今後も継続してふりかえり会を続けていきたいと思います。

参考書籍

森 一樹 (著) 2021/2/17.アジャイルなチームをつくる ふりかえりガイドブック 始め方・ふりかえりの型・手法・マインドセット 翔泳社

【新卒技術研修】24卒メンバーでチーム開発に取り組みました(意識したこと&学んだこと)

こんにちは。エキサイト株式会社でデザイナーをしている齋藤です。 今回は、24卒技術職メンバーが新卒技術研修で約1ヶ月間チーム開発に取り組みましたので、各々が意識したことや学んだことを体験記として記したいと思います。

はじめに

冒頭、技術職メンバーの構成やチーム開発で取り組んだお題についてご紹介します。

メンバー構成

24卒技術職はエンジニア2名、デザイナー2名です。以下のように役割分担をしました。

役割分担の全体図

エンジニア陣は主にバックエンドを担当し、デザイナー陣はテストケース設計兼務やフロントエンド兼務など、UIデザインに加えてそれぞれの得意領域に合わせた領域を担当しました。

チーム開発で取り組んだお題

今回のチーム開発では、エキサイトグループの全社員のメンタル状況を定期的にアンケート形式で集計する社内システムを約1ヶ月間で作りあげることをお題として取り組みました。

具体的に以下の機能を開発しました。

  • 回答対象者向け機能
    • 回答フォーム
    • 過去の回答確認
    • ユーザーマニュアル
  • 管理者向け機能
    • 作成済みフォームを一覧で確認できるダッシュボード
    • フォームの新規作成
    • フォームごとの管理機能
      • 回答対象者一覧の閲覧(回答状況や部署などで絞り込み表示が可能)
      • フォームの公開 / 非公開 設定
      • フォームの削除
      • 回答結果サマリーのダウンロード(CSVファイル)
      • Slack通知(フォーム公開時に全社的なチャンネルに通知投稿&未回答者にリマインドのDM送信)

PHP(Laravel)を用いて開発を行いました。

チーム開発を通じて意識したこと

チーム内のメンバーがそれぞれ意識したことをご紹介します。

デザイナーA

私は常にユーザーの声を忘れないように意識しました。エンジニアリングの知識不足でチーム開発についていけるか不安に思うこともありましたが、その分、技術的な視点ではなくユーザー目線を重視してUXを検討することに注力しました。具体的には、定期的にユーザーと接点を作り、プロダクトへのフィードバックをもらいながら問題点を整理しました。これにより、チーム全体がユーザー視点を意識した開発を進めることができたと思います。

デザイナーB

私は「いつでも開発を引き継ぎできる状態にする」ことを意識しました。UIデザインとフロントエンドを兼務しましたが、開発の初期段階でデザインシステムをFigma・フロントエンドともに構築を行いました。デザインシステムとして、デザイン原則をトークンとして定義したり、汎用的なUIパーツをコンポーネントライブラリとして用意をすることにより、誰でもすぐに一貫性のあるデザインとフロントエンドの実装を行えるような環境を整備しました。結果として、もう一方のデザイナーがUIをデザインをする際にも一貫性のあるUIがスピーディーに仕上げることを可能にし、短い開発期間で完成させることができた一因にもなりました。

エンジニアC

私は、今回のプロジェクトを進めるにあたりバックエンドの業務を初めて行いました。そのため、わからないが多くプロジェクトを止めてしまわないか不安でした。そこで意識したこととしては、悩んだことがあればすぐに共有することです。自分が行き詰まってしまって抱え込んでプロジェクトを止めてしまわないようにすることで、自分ができることできないことをチームメンバーが把握しやすいようにして、フォローし合えるように意識しました。

エンジニアD

(これはチーム開発に限らず、ソフトウェアを開発する際は常に意識していることですが)最初に可能な限りしっかりと設計を行うことを意識しました。具体的には、DB設計やURL設計、画面遷移図の作成などを約2日かけて協力して行いました。最初に設計を行うことによって曖昧な部分を減らすことができ、手戻りの少なさにつながったと思います。

チーム開発を通じて学んだこと

チーム内のメンバーがそれぞれが学んだことをご紹介します。

デザイナーA

今回の研修で、私はテスト仕様書の作成からテスト実行までを担当しました。作成したプロダクトを様々なユースケースでテストするためです。初めての経験だったため、仕様書作成の際には自分が見逃している点や他のケースがないか、チームメンバーに何度もチェックしてもらいました。チームの助けを借りてテストを行ったことで、改善点を早期に発見し、期限内に修正することができました。このプロセスを通じて、メンバーと連携しながら細部まで注意を払う重要性を学ぶことができました。

デザイナーB

うまくいっている時だからこそ、立ち止まってプロジェクト全体を俯瞰して見つめ直すことの重要性を改めて学びました。人間の性でもあると思いますが、うまくいっている時には邁進してしまいたくなってしまいます。立ち止まらずに突き進んでしまうと、思わぬ盲点を見逃してしまうこともあるのだと思います。今回のプロジェクトでは、冷静な目がチーム内にあったため大きな問題は起こりませんでしたが、今後も「慢心は墓穴を掘る」が現実にならないように、うまくいっている時だからこそ、立ち止まる習慣を身につけていきたいと思います。

エンジニアC

共通認識を作ることがプロジェクトを進めるにあたりとても重要であるということを学びました。たとえばデザイナーさんとUIを作る時にエンジニア的に可能かどうか落とし所を考える時やフロントエンドとバックエンドのデータのやり取り、プロジェクトでの必要機能や期日などの認識のすり合わせなどです。共通認識を作ることで、開発の円滑さにつながるとともに、プロジェクトがどの程度進んでいてどのあたりに課題があるかを把握して完成に向けて行動することができました。

エンジニアD

(今回のような日数が限られている開発においては)何日までにこれをやるというスケジュールを決めるのに時間をかけすぎない方が良いと感じました。1つのコントローラには最大でも3日しかかけない程度の大まかなスケジュールだけ決め、すぐに開発に取り掛かったことにより、開発に使える時間が増えて結果としてより良いサービスを作ることができたと思います。

さいごに

今回のチーム開発を通じて、大きな学びを得て、これからの実務への足がかりになったと思います。

これはひとえに、新卒研修の運営メンバーの方々、メンターの方々、研修中に激励とアドバイスに足を運んでくださった方々などのすべての諸先輩方のおかげであると、新卒メンバー一同、改めて感謝をしています。

この経験を糧に、日々の業務に取り組んでいきたいと思います。

ご精読ありがとうございました。

【Dart】Nullの扱い方について

こんにちは。エキサイト株式会社でエンジニアをしている新卒の岡島です。 普段業務ではFlutterを用いたアプリ開発を行っています。 今回は、業務中にnullの扱いについて悩む場面があったので、Dartのドキュメントを読んで良いとされるnullの扱い方をみていこうと思います。

nullとは

nullとは、何のデータも含まれない状態のことです。nullを不適切に扱うとクラッシュするなどバグの原因となることもあるので注意が必要です。

DartにはNull Safetyと呼ばれるnullを健全に扱う機能が備わっています。これにより、Dartはデフォルトでnullを許容せず、nullに関するエラーはコンパイル時に検出されるようになりました。

今回、この記事ではDart公式ドキュメントの中のEffective Dartで触れられているnullについての扱い方を見ていきます。

Effective Dartで推奨されているnullの扱い方

Dartのドキュメントで触れられているnullの扱い方について調べてみました。以下に、具体的なポイントをまとめます。

環境

Dart version: 3.4

明示的にnullを初期化しない

Dartでは変数がNull許容型の場合、変数が暗黙的にnullで初期化されるようになっています。

良いとされるコード

Item? bestItem;

void error([String? message]) {
  stderr.write(message ?? '\n');
}

悪いとされるコード

Item? bestItem = null;

void error([String? message = null]) {
  stderr.write(message ?? '\n');
}

Null許容型はデフォルトでnullとして初期化されるのでわざわざnullで明示的に初期化する必要はないということです。これは関数のオプション引数の場合も同様です。

Null許容型の等価演算では true または false を使用しない

Null許容型のブール式を評価するには、??!= nullを用いて、nullチェックをする必要があります。

良いとされるコード

// nullの場合はfalseにする
if (nullableBool ?? false) { ... }

// nullの場合はfalseにし、変数を型昇格させる
if (nullableBool != null && nullableBool) { ... }

悪いとされるコード

// nullの場合はエラーになる
if (nullableBool) { ... }

// nullの場合はfalseになる
if (nullableBool == true) { ... }

悪いとされるコードの例であるif (nullableBool) { ... } では、コードがnullと関係があることを示していないため、null非許容型と間違える可能性があります。また、nullableBoolがnullの場合、if(nullableBool)はfalseとして評価されるため、if (nullableBool ?? false) { ... }のように明示的にnullの場合はfalseとして扱うことが好ましいとされています。

ただ、条件式の中の変数に対して??を使用しても、Null非許容型に昇格されないため、?? の代わりに明示的な != null チェックを使用することが推奨されています。

変数が初期化されているかどうかを確認する必要がある場合は、late変数を避ける

Dartでは、late変数が初期化されているかどうかを確認する方法がありません。late変数に状態を格納し、変数が設定されたかどうかを追跡する別のbool値フィールドを持つことで、初期化を検出できますが、Dartは内部的にlate変数の初期化状態を保持するため冗長です。通常は変数をNull許容型にする方が明確です。この場合、nullをチェックすることで変数が初期化されたかどうかを確認できます。

Null許容型を使用するための型昇格またはnullチェックを検討する

Null許容の変数がnullではないことを確認すると、その変数はNull非許容型に昇格されます。 型昇格がサポートされているのは以下の場合です。

  • ローカル変数
  • パラメーター
  • プライベートなfinalフィールド

nullチェックパターンの使用

class UploadException {
  final Response? response;

  UploadException([this.response]);

  @override
  String toString() {
    if (this.response case var response?) {
      return 'Could not complete upload to ${response.url} '
          '(error code ${response.errorCode}): ${response.reason}.';
    }
    return 'Could not upload (no response).';
  }
}

こちらの例ではnullチェックパターンを使用することで、メンバーの値が null ではないことが同時に確認され、その値が同じ基本型の新しい Null 非許容変数にバインドされます。

ローカル変数を使用する

class UploadException {
  final Response? response;

  UploadException([this.response]);

  @override
  String toString() {
    final response = this.response;
    if (response != null) {
      return 'Could not complete upload to ${response.url} '
          '(error code ${response.errorCode}): ${response.reason}.';
    }
    return 'Could not upload (no response).';
  }
}

この例ではthis.responseをローカル変数responseに割り当て、その変数に対してnullチェックを行っています。これにより、responseをNull非許容型として安全に扱うことができます。

最後に

nullの扱いはコードの安全性と信頼性を向上させるために知っておく必要があると思い調べてみました。nullチェックや型昇格などにより、安全で読みやすいコードを意識して今後も業務に取り組みたいと思います。

OpenAPI Generatorで生成したAPIクライアントをコンパイルせずに利用する

こんにちは、エキサイト株式会社の平石です。

今回は、OpenAPI Generatorで自動生成したAPIクライアントをコンパイルせずに利用する方法をご紹介します。

はじめに

OpenAPI Generatorと、Javaでの基本的な利用方法は以下のブログをご覧ください。

tech.excite.co.jp

以前のブログでは、自動生成したAPIクライアントをローカルリポジトリに保存する方法をご紹介しました。

tech.excite.co.jp

しかし、この方法ではサーバー環境上で実行する際には、Jarファイルをアプリケーションコードともにアップロードする必要があります。

解決策として、別途他のサーバーやAWS S3のようなものを用意して、そこにJarファイルをアップロードしライブラリの取得先として利用することが考えられます。
この方法も十分選択肢に入りますが、そもそものアップロード先の用意や、Jarファイルをアップロードする仕組みの構築がやや面倒です。

そこで、今回は生成した自動生成ファイルをそのまま利用してみたいと思います。

環境

今回は以下のような環境を利用します。

手順

といっても、非常にシンプルです。

以下の手順で行います。

  1. これまでの方法で一旦生成する
  2. 生成したファイルを実行するために必要な依存関係を確認する
  3. 2で確認した依存関係を自身で管理するbuild.gradleに追加する
  4. OpenAPI Generatorの設定を確認、適宜変更し再生成する
  5. 不要なファイルを削除する

各ステップを詳しく説明します。
なお、今回は最終的に自動生成ファイルを:api:client:sampleプロジェクトに配置します。

今回の記事におけるディレクトリ構成は以下のとおりです。

project_root
    ├ openapi
    │    ┗ build.gradle
    ├ api
    │  ┗ client
    │        ┗ sample  // この配下にAPIクライアントを作成
    ├ project1
    │    ┗ controller
    │        ┗ src/main/java/com/example/controller/SampleController.java
    ┗ build.gradle  // openapiプロジェクト以外の依存関係等を記述

1. これまでの方法で一旦生成する

こちらのブログでご紹介した内容に沿って、APIクライアントを生成します。

tech.excite.co.jp

具体的には、「実際に生成してみる」節まで実行します。

2. 生成したファイルを実行するために必要な依存関係を確認する

APIクライアント生成を実行すると、openapi/clientgenに多くのファイルが生成されています。
この中から、build.gradleMavenを利用している方はpom.xml)の中の依存関係の部分を確認します。

上記記事の設定の場合には、以下のようになっています。

dependencies {
    implementation 'io.swagger:swagger-annotations:1.6.8'
    implementation "com.google.code.findbugs:jsr305:3.0.2"
    implementation 'com.squareup.okhttp3:okhttp:4.10.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0'
    implementation 'com.google.code.gson:gson:2.9.1'
    implementation 'io.gsonfire:gson-fire:1.9.0'
    implementation 'javax.ws.rs:jsr311-api:1.1.1'
    implementation 'javax.ws.rs:javax.ws.rs-api:2.1.1'
    implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
    implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
    implementation "jakarta.annotation:jakarta.annotation-api:$jakarta_annotation_version"
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
    testImplementation 'org.mockito:mockito-core:3.12.4'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
}

3. 依存関係を自身で管理するbuild.gradleに追加する

2で確認した依存関係を、APIクライアントを配置するプロジェクト(今回の場合、:api:client:sample)の設定をおこなっているbuild.gradleに追加します。

なお、これらの依存関係の中には、テストで必要な依存関係もあるので、必要ない場合には削除してください。

今回は以下のようになりました。

build.gradle

project(':api:client:sample') {
    dependencies {
        implementation "com.google.code.findbugs:jsr305:3.0.2"
        implementation 'com.squareup.okhttp3:okhttp:4.10.0'
        implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0'
        implementation 'com.google.code.gson:gson:2.9.1'
        implementation 'io.gsonfire:gson-fire:1.9.0'
        implementation 'javax.ws.rs:jsr311-api:1.1.1'
        implementation 'javax.ws.rs:javax.ws.rs-api:2.1.1'
        implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
        implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
        implementation "jakarta.annotation:jakarta.annotation-api:1.3.5"
    }
}

4. OpenAPI Generatorの設定を確認、適宜変更し再生成する

OpenAPI Generatorの設定を改めて確認します。
上記記事では以下のように設定しました。

openApiGenerate {
    generatorName.set("java")
    inputSpec.set("${rootDir}/specs/test-api-docs.yaml")
    outputDir.set("${projectDir}/clientgen")
    apiPackage.set("com.example.clientgen.api")
    invokerPackage.set("com.example.clientgen.invoker")
    modelPackage.set("com.example.clientgen.model")
    configOptions.set([
         groupId: "com.example",
         artifactVersion: "0.0.1-SNAPSHOT",
    ])
}

今回は、生成したファイルを直接利用するため、build.gradlepom.xmlに反映されるgroupIdartifactVersionをあえて設定する必要はありません。

また、configOptions.set内でhideGenerationTimestamptrueに設定すると、生成ファイル内にタイムスタンプが含まれなくなります。 生成したファイルをGit管理する場合には、余計な差分が生じなくなるためおすすめです。

ここで、outputDir.set"$projectDir/api/client/sample"に設定することで、:api:client:sampleプロジェクト内にAPIクライアントを生成できます。

openapi/build.gradle

openApiGenerate {
    generatorName.set("java")
    inputSpec.set("${rootDir}/specs/test-api-docs.yaml")
    outputDir.set("${rootDir}/api/client/sample")
    apiPackage.set("com.example.clientgen.api")
    invokerPackage.set("com.example.clientgen.invoker")
    modelPackage.set("com.example.clientgen.model")
    configOptions.set([
         hideGenerationTimestamp: "true"
    ])
}

この設定でAPIクライアントを再生成します。

なお、${projectDir}/clientgen内に生成されたファイルを:api:client:sampleにコピーするようにしても構いません。

5. 不要なファイルを削除する

4で生成したファイル内で、srcディレクトリ内のコード以外は不要なため削除しても問題ありません。

実際に利用する際には、APIクライアントの自動生成とともに、src配下以外を自動で削除するようなGradleタスクを作成しておくと良いでしょう。

動作確認

では、コンパイルせずにJavaファイルのまま利用できるかどうか、動作確認をします。

APIを利用するプロジェクトの依存関係に、api:client:sampleプロジェクトを追加します。

build.gradle

project(':project1:controller') {
    bootJar {
        enabled = true
    }

    dependencies {
        // 略

        // 追加
        implementation project(':api:client:sample')
    }
}

APIを呼び出してみます。呼び出し先のAPIは以下のようになっています。
パラメータuserId11111111を渡すことでユーザー情報を取得できます。

@RestController
@RequestMapping("user")
public class UserController {
    @GetMapping
    public UserResponseDto getUser(
            @ModelAttribute @ParameterObject UserRequestDto requestDto
    ) {
        if (Objects.equals(requestDto.userId(), "11111111")) {
            return new UserResponseDto()
                    .setUserId("11111111")
                    .setUserName("白金 高輪")
                    .setEmail("takanawa.shirokane@example.com");
        }

        return new UserResponseDto()
                .setUserId("")
                .setUserName("")
                .setEmail("");
    }
}

呼び出し側は以下の通りです。
APIを呼び出し、取得したユーザー情報からユーザー名を返します。

project1/controller/src/main/java/com/example/controller/SampleController.java

@RestController
@RequestMapping("sample")
public class SampleController {
    @GetMapping
    public String sample() {
        final UserApi userApi = new UserApi();

        try {
            return userApi.getUser("11111111").getUserName();
        } catch (ApiException e) {
            throw new RuntimeException(e);
        }
    }
}

アプリケーションを起動して/sampleにアクセスすると、「白金 高輪」という文字列が返ってきます。

終わりに

今回は、OpenAPI Generatorで生成したJavaファイルをJarに変換せずに直接利用するための手順を紹介しました。
Jarファイルで管理するよりも差分もわかりやすいですし、今後はこちらの方法で運用していくのも良さそうです。

では、また次回。

参考文献

Interop24 Tokyoに行ってきた!

こんにちは、エキサイト新卒3年目、趣味は自宅鯖(オンプレ)のNOGU(@NOGU_D626🐤)です。

本日はInterop24 Tokyo の 13日(2日目)に行ってきたので熱が冷めないうちにレポートとして記事を書きたいと思います!!

Interop Tokyoとはインターネットテクノロジーのイベントです。

会場の様子

会場の入り口

少し歩いて行くと以下のようにInterop24 Tokyo の看板が見えます。これを正面から見て左もしくは右に向かって歩いて行くと、ゲートに立っている人がいるので事前に用意した入場証を見せることで会場に入ることができます。
(明日行く人用に!!)当日でもweb上で申込事項を記入することで入場するができます。

入場したゲートから見た全体の会場の雰囲気は以下の感じです。

会場全体の様子

展示ブースの様子

ネットワークやサーバーに興味があることもあり、このイベントに参加しました。
また、インターンの頃にお世話になったインフラ部の方から、「来年の会社移転に向けてWi-Fi環境を見直したい」との相談を受けていたため、今回のイベントではネットワーク機器メーカーのブースを回りつつ、各社の話を伺うことも目的の一つでした。

VLAN毎に色の振り分けができるスイッチ

抜け防止機構のついたケーブルの展示

抜け防止機構のついたケーブルの展示
ブースの中には、20kgの重りを持ち上げてケーブルの強度を確認する展示もありましたが、ケーブルよりも自分の腕が限界に達しそうでした。実際に体験してみることで、製品の信頼性を実感することができました。

展示の中でも特に目を引いたのは、ラックマウントの展示でした。 全体の写真載せたいところですが、実際の現地にて確認するのが一番だと思うので一部の写真のみ掲載とします。

ラックマウントの展示は実際の利用シーンを想定して実際に稼働しているもので場内のShownetの歩き方というリーフレットを見ながらか、ツアーに参加して話を聞くとより理解が進むと思います。(明日行く方へ)

リーフレット
このリーフレットの中には展示に関するマップ、ラックマウントされている機器の説明なども書かれています。

イベント会場では、ネットワーク機器の展示だけでなく、3大キャリアの回線展示ゾーンもありました。このゾーンでは、NTTコミュニケーションズKDDIソフトバンクの回線が床に展示されており、それぞれの回線の特性や利用用途について紹介されていました

基調講演 / セッション

展示ブースとは別に、展示会場とは別棟の国際会議上ではShowNet参加企業による基調講演なども行われていました。 以下は公式ホームぺージの基調講演のタイムテーブル

の一部になります。

公式ホームページの基調講演のタイムテーブル

展示ブースでは各企業の展示に加え、セッションやセミナーも行われており、私はネットギアジャパン様のセミナーやJPRS様のセミナーなども聞いておりました。これらのセッションは非常に有益で、最新の技術やトレンドについて多くのことを学ぶことができました。

各ブースでいただいたノベルティー

ノベルティー

手のひらネットワーク機器2:逸般の誤家庭で話題の新シリーズ

イベントでは、最新の技術展示やセミナーだけでなく、ユニークなガジェットも注目を集めていました。その中でも特に目を引いたのが「手のひらネットワーク機器2」です。このシリーズは、前作「手のひらネットワーク機器1」に続く第二弾で、ネットワーク機器のミニチュアモデルが手のひらサイズで再現されています。 今回の展示会場では、「手のひらネットワーク機器2」を実際に入手する機会がありました。写真にあるように、Dell TechnologiesやAPRESIA、Extreme Networks、Fortinetなど、著名なネットワーク機器メーカーのモデルが含まれています。 残念ながら自身の時間的制約もあり、1台のみしか入手できず、ラックとして組み立てることはできません。 今後、近所のガチャガチャなどで回す機会があれば、全てをコンプリートし、ラックとして完成させたいと思います。

終わりに

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しています。 また、長期インターンも歓迎しています。

カジュアル面談からもOKなので!!お気軽にご連絡頂ければ幸いです。

▼ 募集職種一覧 ▼ recruit.jobcan.jp

【アプリ開発】データレイヤにおけるリポジトリ層とデータソース層について

こんにちは。エキサイト株式会社でエンジニアをしている新卒の岡島と申します。 普段業務ではFlutterを用いたアプリ開発を行っています。 今回は、既存APIからリビルド後のAPIへの繋ぎ込みを行った際に、アーキテクチャの重要性を痛感したので、アーキテクチャについて勉強したことを共有していこうと思います。

アーキテクチャの重要性を感じた業務中のケース

アーキテクチャの理解が乏しかったため、リビルド後のAPIへの繋ぎ込みを以下のように行っていました。

繋ぎ込み前
繋ぎ込み後

しかし、コードレビュー時に以下のようにすると良いとの指摘を受けました。

このようにすることで、Repositoryはデータソースの向き先だけ変更するだけで済み、下記のようなメリットがありました。

  • ViewModelが参照するRepositoryの変更が不要
  • 移行後は既存APIのDataSourceを削除するだけで済む

要約すると影響範囲が少なくて済むということです。

今までアーキテクチャはなんとなく責務を分離できるためメンテナンスが容易である程度の理解でしたが、このことをきっかけにアーキテクチャへの理解が進みました。 そこで勉強したリポジトリ層とデータソース層についてAndroid Developersで紹介されているデータレイヤに基づいてまとめたいと思います。

Android アプリアーキテクチャにおけるデータレイヤ

ここではAndroid Developersのアプリアーキテクチャガイドで紹介されているデータレイヤについて紹介します。 データレイヤとはデータの取得、操作に関する層であります。

データレイヤにはアプリデータとビジネス ロジックが含まれています。ビジネス ロジックは、アプリデータの作成、保存、変更方法を決定する実際のビジネスルールで構成されており、アプリに価値を提供するものです。

データレイヤでこのように関心の分離を行うことで、複数画面での使用、アプリの各要素間での情報共有ができるほか、単体テスト用に UI の外部でビジネス ロジックを再現することも可能になります。

下記より引用 https://developer.android.com/topic/architecture/data-layer?hl=ja

リポジトリ層の役割

リポジトリ層や以下の役割を担います。

複数のデータソース(リモートAPI、ローカルDBなど)からデータを収集し、必要なビジネスロジックを実行する層がリポジトリ層です。リポジトリ層はデータソース層からデータを収集し、アプリケーションの他の部分に提供しますが、データの取得や保存そのものの操作はデータソース層が行います。

データソース層の役割

データソース層は実際のデータ取得や保存を担当する層であり、各データソース(リモートAPI、ローカルDBなど)ごとに個別に実装されます。この層の主な役割は、外部のデータリソースと直接やり取りを行い、必要なデータを取得したり保存したりすることです。

リポジトリ層との違いは、実際にデータの取得保存などの操作を行う点です。

まとめ

今回は業務中の事例からデータレイヤのリポジトリ層とデータソース層についてまとめました。一言にデータの取得といっても、データソース層からデータを取得するのか、実際にDBやAPIからデータを取得するのかといった違いがあり、リポジトリ層とデータソース層の役割を明確に理解することができました。

【JetBrains製エディタ対応】PrettierでTailwind CSSのclass名を規則的に自動ソーティングさせる

こんにちは。エキサイトでデザイナーをしている齋藤です。

今回は、Prettierを使用してTailwind CSSのclass名を自動でソーティングさせる方法についてお話をします。

VS Codeを使用した環境での情報は多く出回っていますが、IntelliJ IDEAやWebStormなどのJetBrains製エディタを使用している場合についての情報はあまりなかったので、IntelliJ IDEAでの設定方法をご紹介したいと思います。

なぜclass名を規則的に記述したいのか

冒頭、前提としてなぜTailwind CSSのclass名を規則的に記述したいかについて、理由をご説明します。

Tailwind CSSCSSを記述することなく、HTMLタグのclass属性にCSSプロパティに対応したclass名を記述することでスタイリングができるCSSフレームワークです。

BEMなどのCSS設計が不要になることや、スタイルが膨れ上がる心配がないなどの利点がある一方で、複雑なスタイリングをする場合にclass属性が長くなり可読性が落ちるという欠点もあります。

<!-- 不規則に記述したコード -->
<div class="py-1 sm:flex-col px-2 items-center justify-center flex">...</div>

そのため、ルールに則って記述することで自分以外のメンバーがコードを読み解く場合の認知負荷を下げることができます。

<!-- レイアウト→修飾→疑似要素・ブレークポイントの順に規則的に記述したコード -->
<div class="flex items-center justify-center px-2 py-1 sm:flex-col">...</div>

しかしながら、手動で並び替えをすると、メンバー間で認識のズレが発生しやすくなったり、新しいメンバーが手を入れる場合にルールを覚えるのに時間を要したりしてしまいます。

そこで、コードフォーマッターのPrettierを使用することで、規則的なコード整形を自動化することができます。

また、Tailwind CSSが公式プラグインを公開しているため、整形ルールを細かく定義する必要なく簡単に設定することができます。

導入方法

前置きが長くなりましたが、PrettierとTailwind CSS公式プラグインの導入方法についてご説明します。

開発環境

今回はSpring Boot + Thymeleafなプロジェクトに、それぞれnpm版のパッケージをインストールします。今回導入する各パッケージのバージョンは以下の通りです。

├── prettier-plugin-tailwindcss@0.6.1
├── prettier@3.3.1
└── tailwindcss@3.4.3

Prettier本体と公式プラグインをインストールする

npm install -D prettier prettier-plugin-tailwindcss

Prettierの設定ファイルでプラグインの使用を宣言する

Prettierの設定ファイルである.prettierrcに、先程インストールしたprettier-plugin-tailwindcssを使用することを宣言します。

(※.prettierrcが存在しない場合にはnode_modulesと同じ階層に作成してください)

{
    "plugins": [
        "prettier-plugin-tailwindcss"
    ]
}

以上でプロジェクト上の設定は完了です。

エディタの設定

自動整形をを可能にするためにはIntelliJ IDEAの環境設定を変更する必要があります。

環境設定の言語&フレームワークJavaScript→Prettierから以下の通り設定してください。

  1. Manual Prettier configurationをオンにする
  2. Prettierパッケージのパスを指定する(node_modules内のprettierディレクトリを指定)
  3. 「次のファイルに実行」にTailwind CSSを使用するファイルの拡張子を指定する(今回はThymeleafなので.htmlのみ)
  4. 「保存時に実行」をオンにする

設定が完了した環境設定

以上の手順で設定することで、ファイル保存時に自動でclass名がソーティングされます。

並び順は、flexabsoluteなどのレイアウトに影響の大きいものを筆頭に、paddingプロパティなどの修飾的なもの、最後にfocus:などの疑似要素やsm:などのブレークポイントとなります。*1

さいごに

今回はPrettierのみを使用してTailwind CSSのclass名を自動でソーティングさせる方法をご紹介しました。

Tailwind CSSの弱点である可読性をPrettierを使用することで改善することができ、さらにはチーム内の認知負荷を下げる工夫にも繋がります。

これからTailwind CSSを使用される方の一助となれれば幸いです。

ご精読ありがとうございました。

SpringBoot Adminを利用して簡易的なアプリケーションモニタリングツールを導入する

エキサイト株式会社エンジニア佐々木です。メディア事業部ではSpringBootとAWSを使用してサービスを展開しています。簡易的な監視にSpringBootAdminを利用していますので、その紹介になります。

環境

環境は下記になります。

$ java --version 
openjdk 21.0.3 2024-04-16 LTS
OpenJDK Runtime Environment Corretto-21.0.3.9.1 (build 21.0.3+9-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.3.9.1 (build 21.0.3+9-LTS, mixed mode, sharing)

$ ./gradlew --version
------------------------------------------------------------
Gradle 8.5
------------------------------------------------------------

Build time:   2023-11-29 14:08:57 UTC
Revision:     28aca86a7180baa17117e0e5ba01d8ea9feca598

Kotlin:       1.9.20
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          21.0.3 (Amazon.com Inc. 21.0.3+9-LTS)
OS:           Mac OS X 14.4.1 aarch64

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.5)

なぜ簡易モニタリングが欲しかったのか?

メディア事業部ではSpringBootをAWS ECSに載せてサービスを提供していますが、コストの関係でエラーログはCloudWatch Logs、正常系はFireLensでS3に入れてAWS Athena、メトリクスはContainer InsightCloudWatchで見るようなこと切替が発生していました。簡易的でも統合的にモニタリングしたい欲求があるので、SpringBoot系に限定はされてしまいますがSpringBoot Adminというモニタリングツールがあるので、導入してみようと思います。

結果から書きますと、SpringBootAdminを入れることで下記のことを実現することができました。

  • ログがリアルタイムに流れてくる
  • メトリクスはほぼ設定不要で取得できる(yamlで数行)
  • アプリケーション内の設定情報を見ることが可能
  • ログレベルを再起動なしで変更可能
  • スレッドダンプ・ヒープダンプの取得が容易
  • キャッシュ削除をSpringBootAdminから実行可能
  • スケジューラーの設定確認が可能

パッと思いつくだけでもこれくらいあります。ただし、過去データを蓄積する機能はありませんので、その場合は他のモニタリングサービスを使用しないといけません。そしてサーバ費のみとなっております。

SpringBoot Adminの構成について

SpringBoot Adminは、サーバとクライアントでライブラリが分かれています。それぞれで設定が必要になります。

SpringBootAdminのServerとClientの構成

SpringBoot Admin Serverの設定

build.gradle

サーバ側の依存関係は下記を追加するだけです。

dependencies {
    ....
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation "de.codecentric:spring-boot-admin-starter-server:3.2.3"
    ...
}

application.yml

サーバー側のアプリケーション側の設定ファイルもほとんど設定がありません。

spring:
  application:
    admin:
      enabled: true
  boot:
    admin:
      monitor:
        default-retries: 1
      ui:
        # 公開用のURL
        public-url: http://localhost:8080/

以上でサーバ側の設定は完了です。

SpringBoot Admin Clientの設定

クライアント側の設定は、各API、Webフロント、管理面、バッチなどの既存のアプリケーションに下記の設定を追加します。

build.gradle

クライアントの方は下記の依存関係を追加します。

dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-actuator'   // メトリクス収集するためのエンドポイントライブラリ
        implementation "de.codecentric:spring-boot-admin-starter-client:$springBootAdminClientVersion"   // SpringBootAdmin Client
}

application.yml

クライアントのアプリケーション側の設定ファイルは、下記となります。少し多めです。

## アプリケーション名
spring:
  application:
    name: sample-application   # SpringBootAdminServerで表示する
  boot:
    admin:
      client:
        enabled: true
        url: http://localhost:8080  # SpringBootAdminServerのURLを記載します
        auto-registration: true  # SpringBootAdminServerに自身のサーバ自動登録する
        auto-deregistration: true # サーバが落ちたときに自動で削除処理を行う。この設定がfalseだと、サーバが落ちた時にSpringBootAdminServer上で異常として検知されます

## spring-boot-starter-actuator のメトリクス用のエンドポイントでどこまで公開するかを設定する
## ほぼ全公開の設定になります
management:
  info:
    env:
      enabled: true
  endpoint:
    health:
      enabled: true
      probes:
        enabled: true
      show-components: always
    logfile:
      enabled: true
  endpoints:
    web:
      exposure:
        include: '*'

## アプリケーションログの出力設定になります。
logging:
  file:
    name: application.log
  logback:
    rollingpolicy:
      max-history: 7
      max-file-size: 10MB

上記で設定が完了となります。

Server/Clientをそれぞれ起動する

それぞれを起動して、通信ができると下記のような画面が出てくると思います。

SpringBootAdminServerのUI

サーバの状態

サーバの状態は画像の通りになります。gitのコミットIDやビルドバージョン、メモリやCPUが取得できているかなど基本情報は取得できています。

サーバの状態

ログファイルの確認

サーバから出力されているログファイルも取得することができます。

ログレベルの変更

ログレベルの変更も、メソッド単位で可能です。

まとめ

SpringBootAdminは、内部でWebflux/Nettyを使用しているようで、結構な接続数を処理できるようです。SpringBootAdminServer1台で、クライアント50〜100台は処理できるようです。データ蓄積はできませんが、簡易的にみるには十分かなと思います。また、Slack通知などや認証まわりなどのカスタマイズも設定を書けば可能なようです。これから使用していこうと思います。

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

[Java]GoogleのOAuth2.0で受け取ったトークンを取り消す方法[Spring Boot]

はじめに

こんにちは、新卒2年目の岡崎です。今回は、GoogleのOAuth2.0で受け取ったトークンを取り消す方法を紹介します。

環境

openjdk version "21.0.2" 2024-01-16 LTS
OpenJDK Runtime Environment Corretto-21.0.2.13.1 (build 21.0.2+13-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.2.13.1 (build 21.0.2+13-LTS, mixed mode, sharing)
  • Spring Boot
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.1)
  • build.gradle

build.grafleに以下の設定を加えます。

dependencies {
 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

実装例

GoogleのOAuth2.0で受け取ったトークンを取り消すには、ユーザーがアカウント設定にアクセスして取り消す方法やアプリケーション側で取り消す方法があります。今回は、アプリケーション側で取り消す方法を考えます。

アクセストークンを取り消すためのリクエストは以下です。

curl -d -X -POST --header "Content-type:application/x-www-form-urlencoded" \
        https://oauth2.googleapis.com/revoke?token={token}

詳細を知りたい方は、公式ドキュメントをご覧ください。

developers.google.com

このリクエストをJavaで実装します。

アクセストークンの取得

ますは、アクセストークンを取得する必要があります。

@RequiredArgsConstructor
public class  Oauth2Service {
    private final OAuth2AuthorizedClientService authorizedClientService;
    private final ModelMapper modelMapper;
    
    public String getToken(Authentication authentication) {
            final OAuth2AuthenticationToken token = modelMapper.map(authentication, OAuth2AuthenticationToken.class);
            final OAuth2AuthorizedClient currentAuthorizedClient = authorizedClientService.loadAuthorizedClient(
                auth2AuthenticationToken.getAuthorizedClientRegistrationId(),
                auth2AuthenticationToken.getName()
        );
        
        return currentAuthorizedClient.getAccessToken().getTokenValue();
    }
}

これでアクセストークンを取得することができます。

アクセストークンの取り消し

取得したアクセストークンを使い、取り消しを行います。

以下は、GoogleのOAuth2.0で受け取ったアクセストークンを取り消す実装例です。

@RequiredArgsConstructor
public class OAtuh2TokenComponent {
    private final QueryParams queryParams;
    private final RestTemplate restTemplate;
    private final static String GOOGLE_LOGIN_REVOKE_URI = "https://accounts.google.com/o/oauth2/revoke";

    @Override
    public void revokeOatuh2Token(String token) {
        final HttpHeaders httpHeaders = HttpHeadersBuilder.build();
        final HttpEntity<String> entity = new HttpEntity<>(httpHeaders);

        final MultiValueMap<String, String> queryParams = this.queryParams
                .builder()
                .add("token", token)
                .build();

        final URI uri = UriComponentsBuilder
                .fromUriString(GOOGLE_LOGIN_REVOKE_URI)
                .queryParams(queryParams)
                .build()
                .encode()
                .toUri();

        try {
            restTemplate.exchange(uri, HttpMethod.POST, entity, Void.class);
        } catch (Exception exception) {
            throw new InternalServerErrorException("API request exception: " + exception.getMessage(), exception);
        }
    }
}

QueryParamsの実装例です。

@Component
@RequiredArgsConstructor
public class QueryParams {
    private final SpringProfile springProfile;

    /**
     * クエリパラメータを取得する
     *
     * @return クエリパラメータ
     */
    public QueryParamsBuilder builder() {
        return new QueryParamsBuilder("domainパス");
    }

    public static class QueryParamsBuilder {
        private final LinkedMultiValueMap<String, String> queryParams;

        /**
         * コンストラクタ
         *
         * @param selfDomain 自ドメイン
         */
        public QueryParamsBuilder(String selfDomain) {
            this.queryParams = new LinkedMultiValueMap<>();

            this.queryParams.add("domain", selfDomain);
        }

        /**
         * クエリパラメータを追加する
         *
         * @param key キー
         * @param value
         * @return クエリパラメータビルダー
         */
        public QueryParamsBuilder add(String key, String value) {
            this.queryParams.add(key, value);
            return this;
        }

        /**
         * クエリパラメータを取得する
         *
         * @return クエリパラメータ
         */
        public MultiValueMap<String, String> build() {
            return this.queryParams.deepCopy();
        }
    }
}

上記で実装したメソッドを呼び出し、アクセストークンが取り消されたことが確認できれば実装完了です。

最後に

今回は、JavaGoogleのOAuth2.0で受け取ったトークンを取り消す方法について紹介しました。誰かの参考になれば幸いです。

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを募集しております。
興味があれば是非ご連絡いただければと思います!

募集職種一覧はこちら www.wantedly.com

【Flutter】UIのイベントをデバウンスする

こんにちは。エキサイト株式会社 モバイルアプリエンジニアの克です。

今回は、FlutterにおいてUIのイベントをデバウンスする手法についてです。

デバウンスについて

デバウンスとは、短時間に複数回のイベントが発生することを防ぐための手法です。
例えばイベントに対して100msのデバウンスを設定する場合、最後のイベントが発火してから100msが経過したタイミングで初めてそのイベントが通知されます。
100msが経過する前に次のイベントが発火した場合は、そのイベントからさらに100msが経過するまで通知は延期されていきます。

似たものとしてスロットリングがありますが、こちらはまず最初に発火したイベントを通知し、そこから一定時間が経過するまでに発火したイベントは無視するものとなります。

用途としては最新のイベントを重視する場合にはデバウンス、時間あたりのイベント数を制限したい場合にはスロットリングというように使い分けます。

まずはUIを実装する

デバウンスを適用するUIですが、今回はPageViewとSliderを連動させ、Sliderの操作時に選択されたページをPageViewに表示するというものにします。
これは画像のビューアーなどでよく使われる構成です。

まずはこれらのUIを実装していきます。
動作イメージとコードは下記の通りです。

class SliderPager extends HookWidget {
  const SliderPager({super.key});

  @override
  Widget build(BuildContext context) {
    final selected = useState(0.0);
    final pageController = usePageController();
    return Scaffold(
      body: Column(
        children: [
          Expanded(
            child: PageView.builder(
              controller: pageController,
              itemBuilder: (_, index) => Center(
                child: Text(index.toString()),
              ),
              itemCount: 100,
            ),
          ),
          Slider(
            value: selected.value,
            onChanged: (value) {
              pageController.jumpToPage(value.ceil());
              selected.value = value;
            },
            max: 100,
          ),
        ],
      ),
    );
  }
}

値の管理をシンプルにするためにflutter_hooksを使用しています。
選択中の位置はhooksを使いselected 変数で管理しています。

UIイベントにデバウンスを設定する

要件が単純な場合はこのままでも何も問題はないかと思います。
ただし、下記のようなケースではデバウンスを設定したほうがいい場合があります。

・アイテムの表示を計測しており、スライダーのシーク中は計測のイベントを送りたくない
・アイテムが表示された際に重い処理を実行しており、連続で実行されると困る

これらのケースを想定して、スライダーをシークした際のイベントにデバウンスを設定するようにします。

今回は公開されているライブラリのeasy_debounceを使用します。
内部的にはTimerを使用してデバウンスを実現しています。

コードを下記のように変更します。

EasyDebounce.debounce(
  'slider-debounce',
  const Duration(milliseconds: 100),
  () => pageController.jumpToPage(value.ceil()),
);

第1引数はデバウンスのタグで、この値を変えることで複数同時にデバウンスを設定することもできます。
第2引数はデバウンスの待機時間です。長くしすぎると操作時に違和感が出てしまうため調整したほうがいいでしょう。
第3引数が実際に実行する処理となります。

注意点として、selectedの更新にはデバウンスを設定しないようにします。
この値はSliderの表示に使用しているため、リアルタイムで更新する必要があります。

この変更による動作イメージは下記となります。

まとめ

デバウンスは便利な場面もありますが、多用するとUXに悪影響を及ぼします。
そのため、要件との折り合いを考えつつ手段の一つとしてスポットで活用するのがよさそうかなと思います。
様々な実装方法を組み合わせて使いやすいアプリを作っていきましょう。

採用情報

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを募集しております。
興味があれば是非ご連絡いただければと思います!

募集職種一覧はこちら www.wantedly.com

Docker 運用の Terraform に TFLint を導入する

エキサイト株式会社の@mthiroshiです。

運用している Terraform に TFLint を導入してみましたので、設定の方法や lint ルールの一例について紹介します。

TFLint とは

TFLint は、Terraform の lintです。 github.com

HashiCorp 非公式のサードパーティツールですが、Terraform の公式 Style Guide の中で紹介されています。 developer.hashicorp.com

Use a linter such as TFLint to enforce your organization's own coding best practices.

TFLint の設定

運用中の Terraform プロジェクトは、Docker で Terraform を実行していました。 TFLint の導入も Docker で行いました。

Docker の設定

まず、compose.yaml を説明します。

services:
  terraform:
    image: hashicorp/terraform:1.7.5
    volumes:
      - .:/terraform-demo
    working_dir: /terraform-demo
    entrypoint: ["/bin/ash"]
    tty: true

  tflint:
    image: ghcr.io/terraform-linters/tflint
    volumes:
      - .:/terraform-demo
    working_dir: /terraform-demo
    entrypoint: ["/bin/sh"]
    tty: true

ディレクトterraform-demo/ 以下で Terraform コードを管理します。 Terraform を実行する際は、下記のコマンドで Docker 環境にログインします。

docker compose up -d terraform
docker compose exec terraform /bin/ash

TFLint の設定

続いて、TFLint を実行するために .tflint.hcl に設定を記述していきます。 公式の設定を参考にしています。

github.com

plugin "terraform" {
  enabled = true
  preset  = "recommended"
}

plugin "aws" {
  enabled = true
  version = "0.31.0"
  source  = "github.com/terraform-linters/tflint-ruleset-aws"
}

AWS を使っているプロジェクトなので、AWS 用のプラグインを有効にします。

TFLint を実行する

下記で TFLint を実行します。

docker compose run --rm tflint -c "\
    tflint --init;\
    tflint --recursive;\
"

実行結果の例です。

docker compose run --rm tflint -c "\
                tflint --init;\
                tflint --recursive;\
        "
Installing "aws" plugin...
Installed "aws" (source: github.com/terraform-linters/tflint-ruleset-aws, version: 0.31.0)

....

Warning: Missing version constraint for provider "aws" in `required_providers` (terraform_required_providers)

  on main.tf line 107:
 107: resource "aws_route53_record" "record" {

Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.7.0/docs/rules/terraform_required_providers.md

terraform_required_providers のルールについて警告されました。 このルールは、Provider にはバージョンを必須で指定することを求めています。 内容の詳細は、表示されたリンクから確認できます。

次に、--fix オプションをつけて実行します。 --fix オプションでは、いくつかのルールを自動で修正してくれます。

docker compose run --rm tflint -c "\
    tflint --init;\
    tflint --recursive --fix;\
"

--fix オプションで適用されたルールの例です。

  • terraform_deprecated_index
  • terraform_unused_declarations

terraform_deprecated_index のルールは、配列参照のレガシーな記法を指摘するものです。 --fix オプションでは、自動で角括弧の記法に修正します。

github.com

terraform_unused_declarations のルールは、使用されていない変数の宣言を指摘するものです。 --fix オプションでは、自動で変数の宣言を削除します。

github.com

注意点として、自動修正の内容によっては構文エラーが起きます。 例えば、 terraform_unused_declarations の場合、module で定義していた使われていない引数が削除されることがあります。module を参照していた側では、削除された引数を残したままになるため、構文エラーになります。 自動適用後の terraform plan の確認を行いましょう。

最後に

Docker 運用の Terraform プロジェクトに対する TFLint の設定と lint ルールの一例を紹介しました。 運用中のコードに対して lint を実行すると、様々な警告が指摘されると思います。 修正によってはエラーが起きていることがあるので、確認をしながら修正しましょう。

参考になれば幸いです。

採用アナウンス

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアル面談もぜひ!) www.wantedly.com

ModelMapperの使い方

はじめに

こんにちは、新卒2年目の岡崎です。今回は、ModelMapperの使い方について紹介します。

環境

  • Spring boot
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.2)
openjdk version "21.0.2" 2024-01-16 LTS
OpenJDK Runtime Environment Corretto-21.0.2.13.1 (build 21.0.2+13-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.2.13.1 (build 21.0.2+13-LTS, mixed mode, sharing)

ModelMapperについて

知っている人も多いと思いますが、改めておさらいしていきたいと思います。ModelMapperでは、違う型同士の値をコピーし、反映することができます。

例えばAオブジェクトとBオブジェクトがあり、AオブジェクトからBオブジェクトへ値を反映させたいケースがあったとします。この時、ModelMapperを使って行うこともでき、とても便利です。

それでは、実際にコードで見てみましょう!

準備

build.gradleに以下の実装をしてください。

dependencies {
  // model mapper
  implementation 'org.modelmapper:modelmapper:{モデルマッパーの最新バージョン}'

  // lombok
  compileOnly 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'
}

実装

以下のように、NovelDtoオブジェクトとNovelオブジェクトが存在していると仮定します。

@Data
@Accessors(chain = true)
public class NovelDto {
    /**
     * ID
     */
    private Long id;

    /**
     * ユーザーID
     */
    private Long userId;

    /**
     * 高評価数
     */
    private Long likeTotal;

    /**
     * 低評価数
     */
    private Long dislikeTotal;

    /**
     * 高評価したかどうか
     */
    private Boolean isLiked = false;

    /**
     * 低評価したかどうか
     */
    private Boolean isDisliked = false;

    /**
     * 自分の小説かどうか
     */
    private Boolean isMyNovel = false;

    /**
     * 作成日時
     */
    private LocalDateTime createdAt;
}
@Data
@Accessors(chain = true)
public class Novel {
    /**
     * ID
     */
    private Long id;

    /**
     * ユーザーID
     */
    private Long userId;

    /**
     * 高評価数
     */
    private Long likeTotal;

    /**
     * 低評価数
     */
    private Long dislikeTotal;

    /**
     * 高評価したかどうか
     */
    private Boolean isLiked = false;

    /**
     * 低評価したかどうか
     */
    private Boolean isDisliked = false;

    /**
     * 自分の小説かどうか
     */
    private Boolean isMyNovel = false;

    /**
     * 作成日時
     */
    private LocalDateTime createdAt;

    /**
     * オブジェクトが空かどうか判定する.
     *
     * @return オブジェクトが空かどうか
     */
    public Boolean isEmpty() {
        return Objects.isNull(this.commentId);
    }
}

ほとんどのプロジェクトでは層によって使用するオブジェクトが決まっていると思います。例えば、Service層ではRepository層から受け取ったNovelDtoオブジェクトを、Novelオブジェクトへ反映します。

この時の実装例を以下に示します。

@Service
@RequiredArgsConstructor
public class NovelService {
    private final NovelRepository novelRepository;

    public Novel getNovel(Long id) {
        final NovelDto dto = novelRepository.findNovel(id);

        // 中略

        final Novel novel = new Novel()
            .setid(dto.getId())
            .setUserId(dto.getUserId())
            .setLikeTotal(dto.getLikeTotal())
            .setDislikeTotal(dto.getDislikeTotal())
            .setIsLiked(dto.getIsLiked())
            .setIsDisliked(dto.getIsDisliked())
            .setIsMyNovel(dto.getIsMyNovel());

        return novel;
    }
}

このコードの問題点は、setし忘れが起きてバグの原因になる可能性があることです。(もしかすると、このようなコードが煩雑に感じる人もいるかもしれません。)

ModelMapperを使えば、以下のようにコードを書けます。

@Service
@RequiredArgsConstructor
public class NovelService {
    private final NovelRepository novelRepository;
    private final ModelMapper modelMapper;

    public Novel getNovel(Long id) {
        final NovelDto dto = novelRepository.findNovel(id);
        
        return modelMapper.map(dto, Novel.class);
    }
}

一行で書くことができるので、コードはシンプルになり、setのし忘れは起きないようになります。

ちなみにListのオブジェクトの反映は、以下のように行います。

List.of(modelMapper.map(dtoList, 反映したいclass[].class));

補足

ModelMapperを使って開発していると、UnrecognizedPropertyExceptionが発生することがあります。

これはオブジェクト同士を比較した時、存在しない属性があることが原因です。

様々な解決方法があると思いますが、今回は@JsonIgnoreを使う方法を紹介します。

まずは、build.gradleに依存関係を追加します。

dependencies {
        implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:${springMybatisStarterVersion}"
}

そして、オブジェクト同士を比較して存在しない属性に対し、@JsonIgnoreを追加します。

@Data
@Accessors(chain = true)
public class Novel {
    /**
     * ID
     */
    private Long id;

   // 中略

    /**
     * オブジェクトが空かどうか判定する.
     *
     * @return オブジェクトが空かどうか
     */
    @JsonIgnore
    public Boolean isEmpty() {
        return Objects.isNull(this.commentId);
    }
}

これでエラーが発生しなくなりました。

最後に

今回は、ModelMapperの使い方を紹介しました。ModelMapperはとても便利なので、使ってみてください。

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があればぜひぜひ連絡ください!

www.wantedly.com

PHPカンファレンス小田原で初スタッフに挑戦しました!

こんにちは!エキサイト株式会社のまさきちです。

先日、PHPカンファレンス小田原でスタッフしてきました。 今までカンファレンススタッフの経験は無くドキドキでしたが振り返っていきます。

PHPカンファレンス小田原とは

2024年4月13日(土)におだわら市民交流センターUMECOにて開催されたPHPカンファレンスです。(初開催)

PHP関連のイベントは色々な場所で開催されておりますが、小田原の魅力がいっぱい詰まったカンファレンスでした! phpcon-odawara.jp 私は会場&前夜祭お手伝いと、当日スタッフとして参加しました。

会場準備お手伝い

開催日の前日に小田原の会場に行き準備のお手伝いをしました。(実はこの時に初めて小田原に来た)

会場に荷物を運んだり、机と椅子を並び替えたり、配信機材の準備を行いました。 思ったよりやる事が多くて大変でしたが、これから始まるカンファレンスをみんなで作り上げている感じがとても楽しかったです。

前夜祭

前夜祭では受付と写真撮影を担当しました。

受付時にチケットの種類がいくつかあり、条件分岐に戸惑ってしまう場面もありましたが、スタッフで協力し合って乗り切りました。 参加者の皆様にもご配慮いただき助かりました。

受付後にPHPer同士でIRTを行い、写真撮影を担当したのですが、 皆さん盛り上がっていて楽しそうな写真がたくさん撮れて嬉しかったです。

カンファレンス当日

当日は準備や片付けに加え、主に「かま」会場スタッフ、LTタイムキーパーの役割を担当しました。

会場スタッフ

カンファレンスは主に、「かま」と「ぼこ」の二つの会場に分かれて行いました。 「かま」会場では、配信機材や照明の管理、トーク司会、誘導係の3つの役割分担を決めてローテーションしていました。

配信機材を触ったことが無いのでうまく出来るか不安でしたが、操作マニュアルを作っていただけたり、詳しい方が使い方を丁寧に説明してくださっていたので安心して取り組む事ができました。

トーク司会では、登壇者の準備のお手伝いや、タイトル読み上げ、タイムキーパーなど行いました。 こちらも初めての経験で不安でしたが、トークスクリプトを用意してくださっていたので、迷う事なくこなす事ができました。

誘導係は、トークを終えたスピーカーの方を別会場へ案内したり、会場の扉を開け閉めしたり、サブ的な役割をこなしていました。

初めての事ばかりでしたが、コアスタッフの方々が念入りに準備してくださっていたので問題なく対応できました。

LTタイムキーパー

カンファレンス終盤のLTではタイムキーパーを担当しました。 LTには小田原のゆるキャラ「梅丸」が来てくれて大盛り上がり! 私はLT終了時間が来たら梅丸が持っているドラを叩きを担当。(梅丸、ドラ持つの大変だったと思うけどお疲れ様でした) 途中機材トラブルなどもありましたが、周りの方々の助けもあり、無事乗り切る事ができました。

おだわランチ

昼休憩はスタッフの方たちと小田原のランチを楽しみました。 スタッフがお勧めのお店をnoteに書いてくださったので、そちらのお店に行きました。美味しかったし、お店の人優しくて感動。

ふりかえり

カンファレンス終了後にスタッフでふりかえりを行いました。 360°Thanksとして自分を含めたメンバーそれぞれに感謝したり、GoodとMoreを出す作業をMiroを使って付箋に書き出しながら行いました。 振り返ってみると課題もたくさん見つかったので、Nextアクションに繋げていければいいなと思います。

まとめ

初めてのカンファレンススタッフで大変だったけど挑戦してよかった。 カンファレンスの裏側を知る事ができたし、機材や進行などを実体験できて学びが多かった。 PHPerの温かさに再度気づくことができました。 スタッフに興味がある方はぜひトライしてみてください。