「potatotips #86 iOS/Android開発Tips共有会」に参加してきました!

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

potatotips #86 にてLT発表をしてきました。その内容をレポートします!

potatotips #86

potatotips は iOS / Android アプリ開発者向けの勉強会です。

potatotips.connpass.com

#86 は コネヒト株式会社 さん主催で、オンライン・オフラインのハイブリッド開催でした。

会場のコネヒトさんは、エキサイトと同じオフィスビルでしたので、個人的に参加しやすくて助かりました 😁

LTのセッションリストは、下記をご参照ください! github.com

Xでの盛り上がりは、下記のトゥギャッターをご覧ください! togetter.com

懇親会では、アルコール類やピザ等の軽食もご用意していただきました!楽しい会となりました! (写真を撮り忘れてしまったので、こちらのXのポストを引用させてもらいます。)

私のLTスライドはこちらです。Flutterの flutter_sercure_storage パッケージの紹介とAndroid バックアップ機能との利用について注意点を話しました。

感想

個人的には、ネイティブアプリ開発の最新動向を把握できていなかったため、両 OS について幅広く情報をキャッチアップできる機会となりました。特に、iOS のデータ永続化ライブラリである SwiftData に初めて触れることができました。以前に Realm を業務で使用した経験はありましたが、公式のライブラリ提供には安心感を覚えました。懇親会では、各プラットフォームの技術の違いや、他社で採用されている技術について話を聞くことができ、多くの学びがありました。

LT 発表では、久しぶりの発表機会であり、オフラインで雰囲気を共有することのやりやすさを改めて感じました。X でもリアクション頂けたことが嬉しかったです。

potatotips 運営チームの皆様、コネヒト株式会社の皆様、素敵な勉強会をありがとうございました!

採用情報

エキサイトではエンジニアを随時募集しております。ご興味ございましたら、下記の募集一覧ページをご覧ください!

www.wantedly.com

[Java]バーチャルスレッドを導入する方法 [SpringBoot]

はじめに

こんにちは、新卒1年目の岡崎です。Java19でプレビュー機能として提供されていたバーチャルスレッドが、Java21で正式機能として提供されました。今回は、このバーチャルスレッドをSpringBootに導入する方法を紹介します。

バーチャルスレッドについて詳しく知りたい人は、公式ドキュメントをご覧ください。

docs.oracle.com

環境

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)

設定方法

application.ymlに以下の実装を行います。

spring:
  threads:
    virtual:
      enabled: true

これだけでバーチャルスレッドを使用することができます。

確認方法

実際にバーチャルスレッドが使用できるかどうかを確認します。

@RestController
@RequiredArgsConstructor
public class TestController {
    @GetMapping("test")
    public String getThread() {
        return Thread.currentThread().toString();
    }
}

結果

VirtualThread[#97,tomcat-handler-0]/runnable@ForkJoinPool-1-worker-1

バーチャルスレッドが使われていたことが確認できました。

最後に

今回は、バーチャルスレッドをSpringBootに導入する方法を紹介しました。皆さんも使ってみてください。

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

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

www.wantedly.com

AWSのCIDRの指定と、VPC/サブネットにどうIPアドレスが割り当てられるのかについて

エキサイト株式会社メディア事業部所属のエンジニアの岩藤です。

AWSで新しくサービスを作る時に、必ずVPCの作成とサブネットの作成を行うと思います。

CIDRの書き方と、実際にVPCとサブネットにどうIPが割り当てられるかが、今更ながらに理解できてなかったので備忘録です。

CIDRとは

CIDR(サイダー)とは IP アドレス割り当て方法です。

詳しくは下記を参考。

CIDR とは何ですか? - CIDR ブロックと表記の説明 - AWS

CIDR(サイダー)で指定されるIPアドレスの集まりを、CIDRブロックといいます。 CIDRブロックは、111.111.111.111/22のような形式で指定します。 これをプレフィックス表記といいます。

プレフィックスによって、幾つIPが割り当てられるのか

プレフィックス表記の、xxx.xxx.xxx.xxx/ここの数字によって割り当てられるIP数が変わってきます。

awsで固定で使うIPがあるようなので、プレフィックスが/22で1,024個となってもそれが全て使えるわけじゃない事だけ注意。

VPCに実際にどうIPが割り当てられるのか

例えばですが、VPCのCIDRが111.111.60.0/22の場合、下記のIPが割り当てられます。

サブネットのCIDR指定と、どうIPアドレスが割り当てられるかの例

今回は、下記の内容でサブネットを作成するとします。

作成する、アベイラビリティーゾーン(AZ)

AZに作成するprivateサブネットと、publicサブネットにそれぞれいくつIPを割当てるか

その場合、VPCが先ほどの111.111.60.0/22だった場合、CIDRと割当てられるIPアドレスは下記となります。

privateサブネット

publicサブネット

最後に

これさえ理解できていれば、新規サービスを担当した時に VPC/サブネットに対して、プレフィックスをいくつ申請すればいいのか、 どう割当てればいいのか迷わないと思います。

参考してもらえますと幸いです。

Spring BootにおけるRedisのPrimary/Replicaノード判定が自動だったという話

こんにちは。 エキサイト株式会社の三浦です。

以前、Spring Bootにおいて、RedisのPrimary(Master)とReplicaのノードをそれぞれ呼び分けて使うことができる、という記事を書きました。

tech.excite.co.jp

今回はそれに関連して、実はPrimaryノード・Replicaノードの判定が、「コードでの追加順」ではなく「自動」だったというお話をしていきます。

Spring Bootにおける、RedisのPrimary / Replicaノードの呼び分け

Spring Bootでは、Redisを使う際に Primary / Replicaノードを以下のコードで呼び分けることができます。

package sample;

import io.lettuce.core.ReadFrom;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {

        // Primaryノードの設定
        String primaryHost = "localhost";
        Integer primaryPort = 63791;

        // Replicaノードの設定
        String replicaHost = "localhost";
        Integer replicaPort = 63792;

        Integer database = 0;

        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
                .readFrom(ReadFrom.REPLICA_PREFERRED)
                .build();

        RedisStaticMasterReplicaConfiguration serverConfig = new RedisStaticMasterReplicaConfiguration(primaryHost, primaryPort);
        serverConfig.addNode(replicaHost, replicaPort);
        serverConfig.setDatabase(database);

        return new LettuceConnectionFactory(serverConfig, clientConfig);
    }
}

詳しくは、以前書いた以下の記事を御覧ください。

tech.excite.co.jp

これを実行すると、実際の設定値は以下のようになっています。

想定通り、 port: 63791 のノードがPrimary、 port: 63792 のノードがReplicaとして判定されていることがわかります。

ここで一つの疑問が生じました。

このPrimaryとReplicaの判定は、コードでの追加順で決まるのでしょうか?

PrimaryとReplicaの判定は自動的に行われる

それでは今度は、追加順を逆にしてみましょう。

package sample;

import io.lettuce.core.ReadFrom;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {

        // Primaryノードの設定
        String primaryHost = "localhost";
        Integer primaryPort = 63791;

        // Replicaノードの設定
        String replicaHost = "localhost";
        Integer replicaPort = 63792;

        Integer database = 0;

        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
                .readFrom(ReadFrom.REPLICA_PREFERRED)
                .build();

        // 追加順を逆にしてみる
        RedisStaticMasterReplicaConfiguration serverConfig = new RedisStaticMasterReplicaConfiguration(replicaHost, replicaPort);
        serverConfig.addNode(primaryHost, primaryPort);
        serverConfig.setDatabase(database);

        return new LettuceConnectionFactory(serverConfig, clientConfig);
    }
}

結果はこうなります。

ノードの追加順を変更したことでリストの要素の順番は変わっていますが、なんとPrimary / Replica判定は正しく行われています!

このことから、ノードの追加順でPrimary / Replicaが判定されているのではなく、実際にノードの内容から自動的にPrimary / Replicaの判定がされていることがわかりました。

最後に

自動的に判定が行われるということで、アプリケーション開発者としては気にしなければいけないことが一つ減り、良いことなのではないかなと思います。

負荷の分散という意味でノードを分けることは非常に効果的なので、ぜひやってみてください!

Amazon SESでの運用と注意点

エンジニアのAです。今回はAmazon SESについてご紹介します。

AmazonSESとは

Amazon SESはAmazon Simple Email Serviceの略で、SES経由で自身のメールアドレスとドメインを使用してメールを送受信するためのコスト効率の高いメール配信サービスです。

AWS SESは送受信されるメールの量に基づいて料金が発生します。無料枠に関しては、以前は月に62,000通まで無料で送信できていましたが、2023年8月の改訂により

  • 送信 E メール

  • 受信 E メール

  • Virtual Deliverability Manager の送信 E メール処理

の3つのメッセージ料金が毎月最大 3,000 件分無料になります。 (SESの使用開始後の最初の12 か月間)

2024年現在の料金は

  • メール送信 1000件ごとに0.10USD、1GB ごとに0.12 USDの追加料金

  • メール受信 1000件ごとに0.10USD、受信メールチャンク1000 件につき0.09 USDの追加料金

となっています。

最新の料金は下記をご参照ください。 aws.amazon.com

無料枠が減ったとはいえ、月に32KBのメールを200万件送信しても

  • (2,000,000 (メールの件数) - 3,000 (無料枠のメッセージ)) × 0.0001 USD (1 件あたりの料金) = 199.70 USD

  • (0.000032 GB × 2,000,000 - 3,000(無料枠のメッセージ)) × 0.12 USD (データ 1 GB あたりの料金) = 7.67 USD

上記を合わせて207.37USD、日本円換算(145円/ドル)で約3万円に収まります。

これだけ大量のメールを送信してもこの値段で収まり、送信が数万程度ならば数百円程度で済むため、コストパフォーマンスは非常に良いと言えるでしょう。

運用上の注意点

AWS SESにはメールを送れる制限が秒間(最大送信レート)と日間(送信クォータ)が存在しています。

  • 送信クォータ 過去 24 時間に送信したメール数

  • 最大送信レート Amazon SES がアカウントから 1 秒あたりに受け入れることができるメールの最大数

メッセージの送信がアカウントの 1 日の最大数を超える場合、Amazon SES への呼び出しは拒否されます。その為、短期間にメールを大量に送信しようとするとこの送信レートに引っかかる可能性がある為、状況に応じて送信クォータの引き上げをリクエストする必要があります。

引き上げの手順としては、

  1. AWSマネジメントコンソールにサインインし、Amazon SESコンソールを開く

  2. [アカウントダッシュボード]を選択し、送信制限欄にある[制限の引き上げのリクエスト]を押す

  1. サービスクォータの中から、引き上げたいクォータかレートを選択し、[アカウントレベルでの引き上げをリクエスト]を押して、希望する一日あたりの送信クォータか、最大送信レートを入力してリクエス

となっています。

必ずしも承認されるわけではないようなので、クォータ増加リクエストが拒否された場合は、AWS サポートに問い合わせると良いようです。

送信の引き上げを行う際は、[アカウントダッシュボード]の[1 日のE メール使用量]欄を見つつ、送信制限に近づかないように適切に設定しましょう。

また、メールの各種イベントの統計ではなくログを取るには設定が必要です。

参考

repost.aws

Gmailのスパム対策に抵触しないためにもCloudWatchでBounce rate(返送率)やComplaint Rate(苦情率)の閾値設定や監視設定を行った方が良いです。

参考

repost.aws

さらに返送が連続した場合や苦情が発生した場合の処理や通知の設定もしておくと良いでしょう。

PHPでの実装の注意点

実際にSESへのリクエストをPHPのバッチなどで運用している場合、そのまま送信リクエストを送り続けてしまうと最大送信レートを超えてしまうため、usleepなどで秒間のリクエスト間隔を調整する必要があります。

最大送信レートはある程度ならばバーストしても耐えることはできるようですが、長くは続かないようです。

また、他のバッチなどでもリクエストをしていた場合、気付かぬうちに送信レートがバーストしてメールの送信ができないといったことも起こりうるため、SESへの自動送信機能を新たに設定する際は必ず[アカウントダッシュボード]で現在の状態を確認するようにしましょう。

OpenAPI仕様のYAMLファイルからDartコードを自動生成する際に、タグに日本語は使わない方が良いという話

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

エキサイトホールディングス Advent Calendar 2023の22日目を担当いたします。

OpenAPI仕様に基づいてAPIの仕様を定義したYAMLファイルから、アプリ用のDartコードを自動生成する際に、タグの指定で注意すべき点がありました。

実際に、APIとアプリ側の繋ぎ込みを行なう際に問題となったため、ブログとして残しておこうと思います。

OpenAPIとは

本ブログでは、Dartコードを自動生成する部分の説明は省きます。

以下で紹介されていますので、こちらもご覧ください。

tech.excite.co.jp

タグに日本語は使わない方が良い?

結論から述べますと、タグに日本を指定してしまったことで、問題が発生しました。
以下で、日本語を指定することで発生する現象を詳細にご紹介します。

今回はYAMLファイルからコードを自動生成することを考えます。

以下のように、APIを定義したYAMLファイルを用意します。

openapi: 3.0.1
info:
  title: OpenAPI definition
  version: v0
servers:
- url: http://localhost:8190
  description: Generated server url
paths:
  /article:
    get:
      tags:
      - 記事
      summary: 記事取得
      operationId: getArticle
      responses:
        "200":
          description: 正常に処理が終了した場合
          content:
            application/text:
              schema:
                type: string
components: {}

余談ですが、このYAMLファイルは以下のようなSpringBoot製のControllerを持つAPIから生成しました。

@RestController
@RequestMapping("article")
@Tag(name = "記事")
public class ArticleGetController {
    @GetMapping
    @Operation(summary = "記事取得")
    @ApiResponses(value = {
            @ApiResponse(
                    responseCode = "200",
                    description = "正常に処理が終了した場合",
                    content = @Content(mediaType = "application/text", schema = @Schema(implementation = String.class))
            )
    })
    public String getArticle() {
        return "Article";
    }
}

YAMLファイルの11, 12行目でtagsに「記事」というタグを指定しています。

この、tagsに指定した文字列によってエンドポイントが分類されます。
また、自動生成されるDartのコードの「どのクラスに、どのエンドポイントに対応するコードが生成されるか」もこのタグを元に決定されます。

しかし、このタグの設定に上記の「記事」のような英数字以外の文字を入れてしまうと、コード生成がうまくいきません。

この挙動を見てみましょう。

同じ「記事」というタグを設定した記事投稿のエンドポイントと、「ユーザー」というタグを設定したユーザー取得のエンドポイントの定義を追加してみます。

openapi: 3.0.1
info:
  title: OpenAPI definition
  version: v0
servers:
- url: http://localhost:8190
  description: Generated server url
paths:
  /article/post:
    post:
      tags:
      - 記事
      summary: 記事投稿
      operationId: postArticle
      responses:
        "201":
          description: 正常に処理が終了した場合
          content:
            application/text:
              schema:
                type: string
  /user:
    get:
      tags:
      - ユーザー
      summary: ユーザー取得
      operationId: getUser
      responses:
        "200":
          description: 正常に処理が終了した場合
          content:
            application/text:
              schema:
                type: string
  /article:
    get:
      tags:
      - 記事
      summary: 記事取得
      operationId: getArticle
      responses:
        "200":
          description: 正常に処理が終了した場合
          content:
            application/text:
              schema:
                type: string
components: {}

これらのエンドポイントを作成した上で、swagger-ui(http://localhost:8190/swagger-ui/index.html)を確認すると以下のようになっているはずです。

指定したタグによって、「記事」と「ユーザー」で分類がなされていることがわかります。
Swaggerまでは英数字以外のタグも認識してくれるようです。

それでは、アプリ用のDartコードを自動生成してみます。

OpenAPIを使って、Spring Boot製のAPIからアプリ用のDartコードを自動生成する - エキサイト TechBlog. の内容に従って、自動生成を行ないます。

すると、generated/dart/lib/src/api配下にdefault_api.dartファイルが作成されます。

この中身を見ると、すべてのエンドポイントに対応するコードがdefault_api.dart内に生成されてしまっています。

これは、タグを全く指定せずにコードを自動生成した時と同じ結果です。

一方、記事articleユーザーuserのように英数字のみを使うようにすると、以下のように2つのファイルに分かれて生成されます。

このように、dartファイルの自動生成の段階では。英数字のタグしか認識されず、日本語のタグは無視されてしまうようです。
タグがファイル名やクラス名の一部になるという仕様になっているようなので、当然と言えば当然の挙動かもしれません。

もう一つ、以下のような現象も発生します。

「記事API」「ユーザーAPI」というタグを設定した2つのエンドポイントを作成することを考えます。

〜 略 〜

paths:
  /user:
    get:
      tags:
        - ユーザーAPI

〜 略 〜

  /article:
    get:
      tags:
        - 記事API

〜 略 〜

「記事」と「ユーザー」という2つの異なる要素を扱うエンドポイントに対応する自動生成のコードが、別々のファイルに生成されることを期待した設定です。
しかし、この場合、api_api.dartというファイルに2つのエンドポイントに対応するコードが生成されるという、予期しない動作をします。

日本語の部分(「記事」「ユーザー」)が無視され、英数字である「API」という部分だけが認識された結果、どちらも「API」というタグを指定した時と同じ結果になったと考えられます。

最も、全てのコードがdefault_api.dart内等の同じファイルに生成されたとしても、実際のコードには動作上問題ありません。
しかし、自動生成されたコードが分類されていた方が、可読性や利便性の観点で優れていると考えられます。
そのため、タグの指定には英数字以外は使わないようにするのが無難でしょう。

終わりに

今回は、OpenAPI仕様のYAMLファイルからアプリ用のDartコードを自動生成する際には、タグの指定に注意する必要があるということを紹介しました。

では、また次回。

Laravel10 + vue3の環境でInertia.jsを利用する時はresolveComponentを利用する

こんにちは。エキサイト株式会社の奥川です。 本記事ではvue+laravelの環境にInertia.jsを導入する時に発生したエラーの解決方法について紹介します。

環境

Laravel: 10.38.1

Vue: 3.3.13

発生したエラー

Inertiaのドキュメント通りに進め、app.jsに次のように記載しました。ディレクトリ構成の都合上、パスを変更しています。

inertiajs.com

import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'

createInertiaApp({
  resolve: (name) => {
    const pages = import.meta.glob('./components/**/*.vue', { eager: true })
    return pages[`./components/${name}.vue`]
  },
  setup({ el, App, props, plugin }) {
    createApp({ render: () => h(App, props) })
      .use(plugin)
      .mount(el)
  },
})

ここで、下記のようなエラーが発生しました。

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'default')

解決方法

調べてみると、laravelのドキュメントとInertiaのドキュメントに差異があることが分かりました。laravelのドキュメントのようにresolvePageComponentを利用するとエラーが発生しなくなりました。

laravel.com

import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';

createInertiaApp({
  // Inertia.jsのドキュメントの書き方
  // resolve: (name) => {
  //   const pages = import.meta.glob('./components/**/*.vue', { eager: true })
  //   return pages[`./components/${name}.vue`]
  // },

  // Laravelのドキュメントの書き方
  resolve: (name) => resolvePageComponent(
      `/resources/js/${name}.vue`,
      import.meta.glob('/resources/js/components/**/*.vue')
  ),
  setup({ el, App, props, plugin }) {
    createApp({ render: () => h(App, props) })
      .use(plugin)
      .mount(el)
  },
})



Canvaで記事サムネイルを作る方法

はじめに

こんにちは、エキサイト株式会社3年目デザイナーの山﨑です。

エキサイトホールディングス Advent Calendar 2023 23日目は、山﨑が担当させていただきます。

qiita.com

今回は、ブログなどでよく使われる記事サムネイルがどのようにして作られているのか記していきたいと思います。

完成系はこちらです。

タイトルを記入

まず最初にサブタイトルの「誰にも教えたくない」メインタイトルの「最高の旅をする方法」という文章を打っていきます。

フォントを選択し、文字の大きさや位置を調整

この2つの文章のフォントを変更します。

Canvaで和文フォントを使う際は、「Noto San JP」をオススメします。

Noto San JPは視認性が高く、ウェイト(文字の太さ)の種類が豊富なため、同じフォントで太さを変えてメリハリをつけたい時にとても便利なフォントです。

この時、メインタイトル「最高の旅をする方法」の文字サイズはサブタイトル「誰にも教えたくない」より大きく設定します。

メインタイトルの方が情報の優先順位が高いので、サブタイトルと差別化した方がより読みやすいサムネイルとなります。

記事に関連するオブジェクトを配置

今回のタイトルには「旅」というワードが入っているので、「地球」と「飛行機」のオブジェクトを配置します。

Canvaの素材で「地球」「飛行機」で検索して好きなデザインの素材を選択します。

この時のポイントは、なるべく使っている色数が少ないシンプルなデザインを選ぶことです。

サムネイル全体がまとまりのあるデザインになるため、【使用している色が3色以内】の素材を選ぶことをオススメします。

地球は大きい素材なので、半分ほど隠して配置し飛行機は地球の周りを回っているように配置してみました。

この時、無理に地球が全て映るように配置しない方がダイナミックな印象になるよう意識しています。

このように、素材が全て映るように配置するとこじんまりとした印象になってしまうことがあります。

【全体を映さなくても素材を素材と認識できるもの】であれば、多少大きく配置しても問題ないと思います。

色を選定

色はなるべく黒白を除いた3色内に抑えたいところなのですが、今回は黄色・青色・水色・黄緑の4色を使っています。

理由としては、水色は青色の明度を上げたもの(青色の配下)で、黄緑は黄色と青色の中間色なので、極端に色相が異なるものではありません。

3色以上使ってしまっても、色相・彩度・明度のどれかが一致していれば、全体の調和は保たれるのでそのポイントを意識していれば問題ないと思います。

これが、青色・ピンク・黄色・赤など、色相・彩度・明度が異なるものを使ってしまうとデザイン全体にまとまりがなくなってしまうので、ご注意ください。

あしらいを追加

最後に仕上げとして、強調したい文章に中黒を追加、サブタイトル全体を吹出しで囲ってみました。

吹き出しは素材から吹き出しを選択しても良いのですが、今回は「図形」から各丸四角形を選択し、「角丸の丸み」を100にした上で、下に三角形を追加して作っています。

完成

いかがだったでしょうか?

Canvaを使えば簡単に記事サムネイルが作れるので、ぜひ挑戦してみてください。

最後に

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

興味があればぜひよろしくお願いいたします!🙇

www.wantedly.com

【今日から使える!】デザイナーがよく使うあしらいをまとめてみた

はじめに

こんにちは、エキサイト株式会社3年目デザイナーの山﨑です。

エキサイトホールディングス Advent Calendar 2023 25日目は、山﨑が担当させていただきます。

qiita.com

今回は、今日から簡単に使える「デザイナーがよく使うあしらい」について紹介していきたいと思います。

あしらいとは、テキストを強調するための装飾のことを指します。

この文章を強調させたいけどいい装飾が思いつかない…」と悩む時に選択肢の一つとして参考にしていただければ幸いです!

アンダーライン

強調の王道、アンダーライン。

ポイントは少し太めに、文字に掛かるように配置するとより良く見える場合が多いです。

◾️四角形◾️

ほんの少しだけ硬い印象を与えたい時によく使います。柔らかい印象を与えたい場合は丸を使う事が多いです。

"クォーテーションマーク"

強調したい文面群がある場合、クォーテーションマークを入れる事があります。デザイン性を上げるために最近よく使用しています。

\スラッシュ/

補足事項だけではなく、メインタイトルなどでスラッシュは幅広い用途で使う事ができます。点線にすると柔らかく可愛い印象になります。

吹き出し

「第一回」「本当は誰にも教えたくない」「〇〇会員限定」など、補足事項で吹き出しを使う事が多いです。

最後に

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

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

www.wantedly.com

SpringBootでの複数キャッシュサーバがある場合の切り替え

エキサイト株式会社エンジニアの佐々木です。エキサイトホールディングスのカレンダー | Advent Calendar 2023 - Qiitaの9日目を担当させていただきます。SpringBootでキャッシュ先を複数にする場合の設定をご紹介します。

前提

下記、環境で動作検証をしています。

## Java
$ java --version
openjdk 17.0.9 2023-10-17 LTS
OpenJDK Runtime Environment Corretto-17.0.9.8.1 (build 17.0.9+8-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.9.8.1 (build 17.0.9+8-LTS, mixed mode, sharing)

## Gradle
$ ./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:          17.0.9 (Amazon.com Inc. 17.0.9+8-LTS)
OS:           Mac OS X 12.5 aarch64

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

モチベーション

サービスを提供するにあたって、RedisやMemcachedなどの複数キャッシュサーバを使うモチベーションですが下記が考えられます。

  1. トラフィックの増大によりキャッシュといえど負荷やレイテンシーが気になる
  2. セッション情報とコンテンツのキャッシュは分けたい

1. トラフィックの増大によりキャッシュといえど負荷やレイテンシーが気になる を解決する際にリモートキャッシュとローカルキャッシュを併用する場合が考えられます。SpringBootでは、設定ファイル(application.yml)に設定を書くことで単体のキャッシュサーバはすぐに動作するようになるのですが、キャッシュサーバが複数になると設定をコードで書く必要があるので、それをご紹介します。

コード

キャッシュ設定は下記のみになります。

@Configuration
public class CacheConfig {

    @Bean
    @Primary  // <- デフォルトの設定を明示するためにつけています
    public CaffeineCacheManager caffeineCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        Arrays.stream(CacheType.values()).forEach(e -> {
            Cache<Object, Object> cache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(e.getTtl())).build();
            cacheManager.registerCustomCache(e.getName(), cache);
        });
        return cacheManager;
    }


    @Bean(CacheLocalType.LOCAL_CACHE_MANAGER_NAME) // キャッシュ名を明示して定義します
    public CacheManager localCacheManager() {
        ConcurrentMapCacheManager concurrentMapCacheManager = new ConcurrentMapCacheManager();
        List<String> list = Arrays.stream(CacheLocalType.values()).map(e -> e.getName()).toList();
        concurrentMapCacheManager.setCacheNames(list);
        return concurrentMapCacheManager;
    }

}



// ここから下は、キャッシュキーをタイプセーフにしたいのでenumにて定義しているだけです
/**
* 通常キャッシュ用
*/
enum CacheType {

    SECOND_5(CacheType.SECOND_10_KEY, 10), SECOND_10(CacheType.SECOND_20_KEY, 20);

    public static final String SECOND_10_KEY = "SECOND_10";
    public static final String SECOND_20_KEY = "SECOND_20";

    @Getter
    private final String name;
    @Getter
    private final long ttl;

    CacheType(String name, long ttl) {
        this.name = name;
        this.ttl = ttl;
    }
}

/**
* ローカルキャッシュ用
*/

enum CacheLocalType {
    SECOND_5(CacheLocalType.FOREVER);

    public static final String FOREVER = "FOREVER";

    public static final String LOCAL_CACHE_MANAGER_NAME = "localCacheManager";

    @Getter
    private final String name;

    CacheLocalType(String name) {
        this.name = name;
    }
}

上記のように複数キャッシュの設定はあわせても20行以内のコードになります。各キャッシュサーバの設定をメソッド内で行うだけになります。下記は、使用例になります。SpringBootでキャッシュを使う場合は、アノテーションを使用したキャッシュ指定が便利です。@Cacheableは、キャッシュがあれば使用する、なければメソッドを実行し、結果をキャッシュに保存してくれる便利なアノテーションになります。

    @Cacheable(cacheNames = CacheType.SECOND_10_KEY)    // cacheManagerの設定がない場合は、@Primaryの設定が使用されます。
    @Override
    public String useMainCache() {
        return "MainCache created at:" + LocalDateTime.now();
    }

    @Cacheable(cacheNames = CacheLocalType.FOREVER, cacheManager = CacheLocalType.LOCAL_CACHE_MANAGER_NAME)  // cacheManagerの設定がある場合は、@Beanで設定したBean名の設定が使用されます。
    @Override
    public String useLocalCache() {
        return "LocalCache created at:" + LocalDateTime.now();
    }

上記のように@Cacheable などのアノテーションで指定したCacheManagerが採用されてキャッシュされます。宣言的でコードを汚さなくて済むので我々の事業部ではよく使用します。

まとめ

サービス規模が大きくなったり、トラフィックが増えるとこのように複数のミドルウェアを使用することがよくあります。ローカルキャッシュやリモートキャッシュを適宜使い分けができるとより様々な要件に対応できるようになるかと思います。

最後に

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

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

[Java] どこで実装をするかで可読性が異なった話

はじめに

こんにちは、新卒1年目の岡崎です。エキサイトホールディングス Advent Calendar 2023の25日目を担当します。

最近、記事に対する画像URLの変換処理を実装しました。この実装をどこでするかで可読性が異なったので、備忘録として記事に残します。

環境

openjdk version "17.0.2" 2022-01-18
OpenJDK Runtime Environment Temurin-17.0.2+8 (build 17.0.2+8)
OpenJDK 64-Bit Server VM Temurin-17.0.2+8 (build 17.0.2+8, mixed mode)
.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.6)

前提

記事が持っている画像に対して、仕様変更で画像URLの変換処理が必要になったと仮定します。

改善前の実装

画像用の処理を集約したImageComponentクラスに、画像URLの変換を行う処理がありました。これをService層で呼び出して変換を行うと、以下のような実装になります。

@Service
@RequiredArgsConstructor
public class SampleService {
    private final SampleRepository sampleRepository;
    private final ImageComponent imageComponent;

    public ArticleModel getArticle(final String articleCode) {
        return this.convertImageUrl(sampleRepository.findArticle(articleCode));
    }

    /**
     * 画像URLの変換処理を行い、記事にセットする
     *
     * @param articleModel 記事
     * @return 画像URLの変換処理を行い、セットした記事
     */
    private ArticleModel convertImageUrl(ArticleModel articleModel) {
        final String imageUrl = imageComponent.convertImageUrl(
                articleModel.getImageUrl()
        );

        articleModel.setImageUrl(imageUrl);

        return articleModel;
    }
}
@Repository
@RequiredArgsConstructor
public class  SampleRepository {
    private final SampleMapper sampleMapper;
    private final ModelMapper modelMapper;

    public ArticleModel findArticle(String articleCode) {
        // DB等から記事をとってくる処理
        Article article = sampleMapper.findArticle(findArticle(articleCode));

        return modelMapper.map(article, ArticleModel.class);
    }
}
@Data
public class ArticleModel {
    private String articleCode;

    private String imageUrl;
}
public class ImageComponent {

    public String convertImageUrl(String imageUrl) {
        // ここで画像URLの変換処理を行う
    }

}

このコードには問題点があります。それは、Service層で画像URLの変換処理を行なってしまうことです。

この実装では、Modelの構造が複雑になるほど、Service層の可読性が低下します。
現在のModelは複雑ではありませんが、Modelの構造が複雑になるにつれて、Service層で行う変換処理が増えてしまいます。この結果、Service層の複雑性が高まってしまい、可読性の低下を招いてしまいます。

改善策

それでは、どうしたらいいのでしょうか。

改善策の一つは、Model内で実装をすることです。実装例を以下に示します。

@Data
public class ArticleModel {
    private String articleCode;

    private String imageUrl;

    public String getImageUrl() {
        return ImageComponent.convertImageUrl(this.imageUrl);
    }
}
@Service
@RequiredArgsConstructor
public class SampleService {
    private final SampleRepository sampleRepository;

    public ArticleModel getArticle(final String articleCode) {
        return sampleRepository.findArticle(articleCode);
    }
}

Model内で画像URLの変換処理を実装することで、Modelを作るタイミングで変換処理を実行できます。前述した実装のように、いちいちModelを分解して、セットし直す必要はなくなります。

まとめ

今回の場合、Modelに閉じ込めることができる処理を、わざわざService層でやろうとしたことが問題でした。同じような処理でも、どこに実装をするかで可読性は変わることがあります。実装をする時は愚直にやるのではなく、どこに実装をすれば最善なのかを考えて行う必要だと改めて感じました。

また、この記事は、先輩エンジニアのMさんが指摘して下さった内容で書いています。Mさん、今回もありがとうございました!精進します。

最後に

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

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

www.wantedly.com

【2023年12月Firefox対応!】CSSの擬似クラス:has()とは?

こんにちは。

エキサイトで内定者アルバイトとしてデザイナーをしている齋藤です。

今回は2023年12月にFirefox(121)が対応したことにより主要ブラウザが完全対応となった、CSSの擬似クラス:has()についてその概要や使用方法についてお話したいと思います。

Firefoxもついに対応

Firefox120以前では:has()は対応していませんでしたが、今月リリースされたFirefox121ではついに対応することになり、主要ブラウザでは完全対応となりました。

2023年12月25日(メリークリスマス~)時点の各ブラウザでの対応状況は以下の通りです。

ブラウザ バージョン
Google Chrome 105以降
Microsoft Edge 105以降
Safari 15.4以降
Firefox 120以降
Opera 104以降
iOSSafari 15.4以降
AndroidGoogle Chrome 105以降
AndroidFirefox 121以降
AndroidOpera 72以降
Android WebView 105以降

なお、対応状況は今後変化する可能性があります。使用前にCan I use...などで確認してください。

そもそも:has()とは?

そもそも:has()とはどんな擬似クラスなのでしょうか。MDNでは以下のように紹介されています。

:has()CSS の疑似クラス関数で、引数として渡される相対セレクターのいずれかが、その要素から辿ってアンカーとして少なくとも一つの要素とマッチする場合に、その要素を表します。この疑似クラスは、相対セレクターリストを引数として取ることで、参照している要素に関して親要素や前の兄弟要素を選択する方法を提供します。

簡単に言い換えると、:has()は指定した要素がある場合にのみスタイルを適用できる擬似クラスです。

条件分岐については、

  • 要素の存在の有無
  • 要素の個数
  • 要素の属性の値

によって適用するスタイルを変化させることができます。

どうやって使うの?

前項でもご説明した通り、:has()を使用することにより特定の要素の有無などで条件分岐をすることにより適用するスタイルを変更することができます。

例えば、「3つ並ぶアイテム内の画像のうち、xmas.jpgという画像を含む要素のみ境界線色を赤に変えたい」としましょう。

実現したいこと

スタイルの条件分岐なしに作ると

まずはスタイルの条件分岐なしに作ってみましょう。

画像付きのアイテム(.item)を3つ並べて、それぞれ左から順にhalloween.jpgxmas.jpghalloween.jpgと画像を挿入します。

<ul>
    <li class="item">
        <img
        alt=""
        class="item__image"
        src="halloween.jpg"
        height="320"
        width="480"
        />
    </li>
    <li class="item">
        <img
        alt=""
        class="item__image"
        src="xmas.jpg"
        height="320"
        width="480"
        />
    </li>
    <li class="item">
        <img
        alt=""
        class="item__image"
        src="halloween.jpg"
        height="320"
        width="480"
        />
    </li>
</ul>

スタイルの条件分岐なしに作った場合

このままですと境界線の色は同じままです。

:has()を使って条件に合った要素のスタイルを変える

今回のお題は「xmas.jpgという画像を含む要素のみ境界線色を赤に変えたい」ですので、画像のクラス名と属性を.itemの擬似クラス:has()の引数に指定することで条件分岐をすることができます。

引数は.item__image[src='xmas.jpg']と、クラス名→属性の順に指定します。

Google Chrome 120からPure CSSでもCSS Nestingに対応しているため入れ子にしています。

.item {
    &:has(.item__image[src='xmas.jpg']){
            border: 2px solid red;
    }
}

:has()を適用させた場合

xmas.jpgの画像を含む要素のみ境界線色を変えることができました。

さいごに

今回はCSSの擬似クラス:has()についてその概要や使用方法についてご紹介しました。

エキサイトホールディングス Advent Calendar 2023では弊社のエンジニア・デザイナーによる記事が多数公開されていますので、ぜひご覧ください!

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

参考文献

第7回テクデザBeer Bashを開催しました

こんにちは。エキサイトでエンジニアをしている吉川です。

エキサイトホールディングス Advent Calendar 24日目の記事になります。

先日12/15(金)に社内交流イベントである「テクデザBeer Bash」の第7回を開催したので、そのレポートを書いていきます。

前回の様子はこちらからご覧ください! tech.excite.co.jp

テクデザBeer Bashとは

テクデザBeer Bashは、主に社内のエンジニアやデザイナー同士のコミュニケーション活性化のためのイベントです。

beer(ビール)+ bash(にぎやかなパーティー)を合わせた造語で、真面目な部分を残しつつ、社員同士がカジュアルな雰囲気で交流を行うことが目的としています。同じチーム内の人はもちろん、業務ではあまり関わることがない他部署の人たちとも繋がる場でもあります。

当日の様子

今回は、

  • 技術ブログの取り組みプレゼン
  • 2023年振り返りクイズ大会

の2つをメインコンテンツとして、終了後はフリートークの場としました。

技術ブログの取り組みプレゼン

アドベントカレンダーを始め高頻度に更新されている当テックブログですが、毎回ネタを考えるのは大変です。

エンジニアの中でも特に執筆いただいている方から、テックブログは自分自身のために書いているということ、テックブログをさらに盛り上げるために実践していることを発表していただきました。

来年はもっと多くの記事を発信できるよう、私個人としても身になる発表でした。

2023年振り返りクイズ大会

昨年も行った振り返りクイズ大会ですが、好評だったため今年も開催させていただきました。

今年1年を振り返って、インターン参加人数やGitHubのプルリクエスト数などエキサイトにまつわる数字について出題しました。 また生成AI画像のように今年話題になった技術についての出題も行いました。

クイズ大会はチーム対抗にしており、優勝チームにはケーキを進呈しました! 今年も好評だったので、年1回はこういうエンタメに寄った企画があっても良いなと感じました。

さいごに

いかがだったでしょうか。社内の雰囲気を少しでも感じていただけたら幸いです。

昨年度から始まったこの企画ですが、ようやく社内の知名度も得られ、軌道に乗ってきた感触があります。 2024年はいよいよ2桁台になる予定ですので、引き続き活気のあるイベントにしていきたいと思います!

エキサイトではデザイナー、エンジニアを絶賛募集しております! ご興味があればこちらからご連絡ください!

www.wantedly.com

IntelliJでThymeleafの補完が効くように設定する

エキサイト株式会社メディア事業部佐々木です。エキサイトHDアドベントカレンダー2023の15日目を担当させていただきます。メディア事業部では、SpringBoot/ThymeleafでMPAの開発を行っているところもあるのですが、HTML上での補完の効きが悪いというのがあります。これを設定することである程度補完が効くようになります。その設定をご紹介します。

前提

## Java
$ java --version
openjdk 17.0.9 2023-10-17 LTS
OpenJDK Runtime Environment Corretto-17.0.9.8.1 (build 17.0.9+8-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.9.8.1 (build 17.0.9+8-LTS, mixed mode, sharing)

## Gradle
$ ./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:          17.0.9 (Amazon.com Inc. 17.0.9+8-LTS)
OS:           Mac OS X 12.5 aarch64

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

Thymeleafの式構文を補完する

Thymeleafの式構文(th:textなどth:がつくもの)を補完してくれるのは、xmlns:th="http://www.thymeleaf.org"<html> タグ内に入れれば補完が効くようになります。

thの補完

ThymeleafでPOJO(Plain Old Java Object)を補完する

上記だけだと、Thymeleafの式構文を補完するのみになります。結構便利になりましたが、for-each等でメソッドや変数のアクセスに補完を使いたくなります。@ThymesVarを使用して、Thymeleaf上でサーバ側で設定したPOJOを補完できるようにします。、

シンタックスは下記のようになります。

<!--/* @thymesVar type="${POJOの型}" id="${変数名}" */-->

型は、フルパスで書く必要があります。サンプルでは下記のようにしました。

<!--/* @thymesVar type="java.util.List<jp.co.excite.adventcalendar.ThymeleafDemoController.Info>" id="infoList" */-->

このように書くとIntelliJで補完がされます。

まとめ

最近はフロントエンドの技術選定はReactやVueが多いですが、ちょっとした管理面などには、構築に手間がかからないMPAがまだまだ便利だったりします。Javaを書く時にIntelliJは大変便利なIDEです。それをテンプレートエンジンにも適用できたらもっと便利になります。@thymesVarsを使用して、少しでも書きやすくなればと思います。

最後に

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

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

JavaでのEnumの実装の仕方と、よく使用するメソッド

はじめに

こんにちは。新卒1年目の岡崎です。エキサイトホールディングス Advent Calendar 2023の21日目を担当します。

よろしければ他の記事もどうぞ!

qiita.com

最近の業務ではEnumを使って実装をしました。なので、今日は改めてJavaEnumについて紹介していきたいと思います。

環境

openjdk version "17.0.2" 2022-01-18
OpenJDK Runtime Environment Temurin-17.0.2+8 (build 17.0.2+8)
OpenJDK 64-Bit Server VM Temurin-17.0.2+8 (build 17.0.2+8, mixed mode)

Enumの実装

Enumは、「列挙型」と呼れています。

Enumの基本的な実装は以下のようになります。

public enum BookType {
    SF(1, "SF"),
    ESSAY(2, "エッセイ"),
    HISTORY(3, "歴史")
    ;

    // フィールドの定義を行う
    private final Integer id;
    private final String type;

    // コンストラクタの定義を行う
    BookType(final Integer id, final String type) {
        this.id = id;
        this.type = type;
    }
}

また、フィールド・コンストラクタの定義をせずに実装することもできます。

public enum BookType {
    SF,
    ESSAY,
    HISTORY
    ;
}

Enumで用意されているメソッド

JavaEnumではさまざまなメソッドが用意されていますが、その中でも私がよく使うメソッドを3つ紹介します。

name

name()では、Enumで定義したオブジェクトの名前を、宣言された通りに返します。

public class Main {
    public static void main(String[] args) throws Exception {
        String name = BookType.ESSAY.name();
        
        System.out.println(name);
    }
}

結果

ESSAY

values

values() では、Enumで定義したオブジェクトを全て返します。

public class Main {
    public static void main(String[] args) throws Exception {
        BookType[] list = BookType.values();
        
        Arrays.stream(list)
        .forEach(bookType -> System.out.println(bookType));
    }
}

結果

SF  

ESSAY  

HISTORY  

valueOf

valueOfでは、引数で指定した文字列から、Enumで定義したオブジェクトを取得できます。

public class Main {
    public static void main(String[] args) throws Exception {
        BookType essay = BookType.valueOf("ESSAY");
        System.out.println(essay);
    }
}

結果

ESSAY

もし、引数で指定した文字列からEnumで定義したオブジェクトを取得できなかった場合、IllegalArgumentExceptionが返ります。

public class Main {
    public static void main(String[] args) throws Exception {
        BookType essay = BookType.valueOf("AAA");
        System.out.println(essay);
    }
}

結果

Exception in thread "main" java.lang.IllegalArgumentException: No enum constant Main.BookType.AAA
    at java.base/java.lang.Enum.valueOf(Enum.java:273)
    at Main$BookType.valueOf(Main.java:9)
    at Main.main(Main.java:5)

補足: Enumを使わなかった実装と、Enumを使った実装の比較

ここまで、Enumの実装の仕方と、よく使用するメソッドを3つ紹介しました。
それでは、補足としてEnumを使わなかった実装と使った実装を比較します。

前提として、IDから本のタイプを取得するようにします。

まずは、Enumを使わなかった場合の実装例です。

public class Main {
    public static void main(String[] args) throws Exception {
        final String type = getBookType(1);
        System.out.println(type);
    }
    
    private static String getBookType(Integer id) {
        if (id.equals(1)) {
            return "SF";
        }
        
        if (id.equals(2)) {
            return "歴史";
        }
        
        if (id.equals(3)) {
            return "エッセイ";
        }
        
        return "";
    }
}

結果

SF

これを、Enumで実装すると以下のようになります。

// Enumに加える実装です

    static Map<Integer, BookType> map = Arrays.stream(BookType.values())
        .collect(
            Collectors.toMap(
                booktype -> booktype.id,
                booktype -> booktype
            )
        );

    public String getType() {
        return type;
    }
    
    public BookType getBookTypeById(Integer id) {
        return map.get(id);
    }
public class Main {
    public static void main(String[] args) throws Exception {
        BookType type = BookType.getBookTypeById(1);
        System.out.println(type.getType());
    }
}

結果

SF

このようにEnumを使うと、変数を代入する場所を限定することができたり、少ない行数で実装することができます。
これにより、可読性の向上や実装ミスを減らすことが期待できます。

最後に

今回は、JavaでのEnumの実装の仕方と、よく使用するメソッドを3つ紹介しました。Enumは上手く使うと、楽に実装をすることができます。ぜひ使ってみてください。