はじめに
初めまして、dodaX エンジニアリンググループの上池です。
dodaX では、マイクロサービス指向でアーキテクチャが設計されており、各サービスは Spring Boot で実装されています。
最近、インフラが Kubernetes ベースに刷新されたこともあり、コンテナ技術に興味が湧いてきました。
そこで今回はコンテナファーストをうたっている Java のフレームワーク Quarkus について調査し、簡単な RESTfulAPI を作ってみました!
Quarkus の特徴や Spring Boot との比較についても解説しています。
この記事は Spring Boot は知っているけれども、Quarkus や GraalVM には初めて触れる方に向けた入門記事です!
TL;DR
- Quarkusの起動時間は Spring Bootよりも早い
- Quarkusは起動時ではなくビルド時にBeanのスキャン、依存関係の解決を行っている
- JITコンパイルが不要な「ネイティブ実行可能ファイル」の生成がサポートされている
- 設定不要ですぐにネイティブ実行可能ファイルを生成できる
- Spring Bootも バージョン3.0からネイティブ実行可能ファイルの生成がサポートされているが、Quarkusよりは起動時間が遅い
- Quarkusアプリケーションの実装方法はSpring Bootとそこまで変わらない
- QuarkusでもSpring同様にDIとAOPがサポートされている
Quarkus の起動時間
いきなりですが、Quarkus の起動時間と Spring Boot の起動時間を比較してみます。
5 回試行の平均をとってみました。
Application | StartUp Time |
---|---|
Quarkus (Native Image) | 0.023s |
Spring Boot on JVM | 1.04s |
Quarkus の起動時間、めちゃくちゃ早いです...!!
※後述しますが、ここでは Quarkus アプリケーションをネイティブコンパイルし、「ネイティブ実行可能ファイル」を実行しています。
Quarkus とは
Quarkus とは、RedHat が主導で開発している オープンソースの Web フレームワークです。
Quarkus はコンテナファーストの Web フレームワークとして設計されていて、
起動時間やメモリフットプリントを最適化することに重点が置かれています。
なので、クラウド環境での利用に非常に適していると言えますね!
Quarkus は便利なツールも豊富に備えています。
例えば、QuarkusCLI を使用すれば、アプリケーションの作成からデプロイまで、簡単かつ効率的に行うことができたり、 Kubernetes の設定ファイルを自動作成することなんかもできたりします。
また、Dev UI と呼ばれるツールも存在し、ブラウザからアプリケーションの設定を変更できたり、使用しているライブラリを GUI で見ることができます。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/k/kamiikepersol/20231205/20231205094330.png)
開発者ツールが充実していて開発しやすそうです...!
Quarkus の起動時間が速い理由
冒頭でも記述したように、Quarkus は起動時間が早いです。
では、なぜ起動時間が早いのでしょうか?
ビルド時での最適化
Spring Boot(Spring)の主要な機能に DI(Dependency injection)と AOP(Aspect Oriented Programming) があると思いますが、 Quarkus でも DI と AOP がサポートされています。
Spring Boot のDIは 実行時(特に起動時)に Bean のスキャンと依存性の解決が行われ、インスタンス生成および DI コンテナへの登録が行われます。これが Spring Boot の起動に時間がかかる原因の一つです。
しかし、 Quarkus ではこの手続きをビルド時に実行します。
つまり、Quarkus の DI ではビルド時に Bean のスキャンと依存関係の解決を行なっています。
Spring Boot のAOPは、例えば@Transactional
をメソッドに付与すると、Spring は CGLIB を使って動的プロキシクラスを生成します。これは Spring Boot の実行時に行われます。
一方で、Quarkus ではビルド時にプロキシを生成してコンパイルします。
ビルド時にどのクラスが何のプロキシを使用するのかを解析します。静的にプロキシを生成します。
さらに、Spring ではリフレクションを多用していますが、Quarkus はリフレクションをなるべく使わない設計方針を採用しています。 なので、起動時間を短くするための静的解析が着実に行われます。
GraalVM でのネイティブ実行可能ファイル生成をサポートしている
Quarkus は GraalVM での ネイティブ実行可能ファイルの生成をサポートしています。
そもそも、JVM 上で動く Java アプリケーションでは、Java クラスファイルを実行時に JVM がコンパイルしてアプリケーションを実行しています。つまり JIT(Just-In-Time) コンパイルしています。
その点、GraalVM (の中の Graal)を使うと
Java のソースファイルを AOT(Ahead-Of-Time) コンパイルすることができ、実行時に JVM による JIT コンパイルを不要とするネイティブ実行可能ファイルを作成できます。
(GraalVM 自体はネイティブ実行可能ファイル生成だけでなく、JVM として動かすこともできます。)
Quarkus は GraalVM でネイティブ実行可能ファイルを生成することを考慮して設計されているので、開発者は基本的に特に意識することなく Quarkus アプリケーションをネイティブ実行可能ファイルに AOT コンパイルすることができます。
※後述しますが、SpringBoot についても バージョン 3.0 からネイティブ実行可能ファイルの生成がサポートされています。
ということで、Quarkus は起動時間を早くするために以上のようなアプローチを行っています。
起動時間が早くなれば、開発者の生産性が上がる・クラウドリソースをスケールしやすい・サーバレスで使えるなどのメリットがあるので嬉しいですね!
Quarkus で RESTfulAPI を実装してみる
ではこのQuarkus を使って、データを登録・取得できるAPIを実装してみたいと思います。
前提
Java: 21 Quarkus: 3.6.0 OS: macOS 13.3
まずは環境構築をして動作させてみる
Quarkus CLI をインストールする
まずはQuarkus CLI
をインストールします。
日本語のドキュメントがあるのはありがたいですね。
brew でインストールします。
brew install quarkusio/tap/quarkus
念のためインストールできたことを確認しておきましょう。
~ $ quarkus --version 3.6.0 ~ $ which quarkus /opt/homebrew/bin/quarkus
これでQuarkus CLI
が使えるようになりました。
Quarkus プロジェクトの作成
では、さっそくプロジェクトを作成してみましょう。
~/sandbox $ quarkus create app jp.co.persolcareer:quarkusdemo --gradle
- このコマンドで以下の指定を行っています。
- groupId: jpco.persolcareer
- artifactId: quarkusdemo
- build tool: gradle
ディレクトリ配下にquarkusdemo
プロジェクトが作成されました。
~/sandbox $ ls quarkusdemo
プロジェクトの中を見てみると、src/main
配下にdocker
ディレクトリが生成されているのが面白いですね!
~/sandbox/quarkusdemo $ ls src/main docker java resources
QuarkusCLI
で Quarkus アプリケーションをコンテナイメージにビルドすることができるようで、ここにはそのときに使用される設定が書かれているようです。
起動してみる
プロジェクトを作成した時点で文字列を返すAPIが作成されています。
作成されたアプリを開発モードで起動してみます。
~/sandbox/quarkusdemo $ quarkus dev
お、立ち上がりました。
BUILD SUCCESSFUL in 11s 7 actionable tasks: 6 executed, 1 up-to-date Listening for transport dt_socket at address: 5005 __ ____ __ _____ ___ __ ____ ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ 2023-12-05 09:14:44,818 INFO [io.quarkus] (Quarkus Main Thread) quarkusdemo 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.6.0) started in 0.833s. Listening on: http://localhost:8080 2023-12-05 09:14:44,820 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated. 2023-12-05 09:14:44,820 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy-reactive, smallrye-context-propagation, vertx] -- Tests paused Press [e] to edit command line args (currently ''), [r] to resume testing, [o] Toggle test output, [:] for the terminal, [h] for more options>
http://localhost:8080/hello にリクエストを投げてみます。
~ $ curl -w "\n" http://localhost:8080/hello Hello from RESTEasy Reactive
レスポンスが返ってきました!
インタフェース を実装する
プロジェクト作成時に生成されているソースを見つつ、編集していきます。 まずはインタフェースを見てみましょう!
プロジェクト作成時にGreetingResource.java
が生成されています。
JakartaREST(旧 JAX-RS) 仕様の実装である RESTEasy を利用して書かれています。
この Resource クラスですが、RESTEasy では裏で CDI を利用しているようです。
Spring Boot(Spring MVC)では、DispacherServlet
に見つけてもらうために、@RestController
をつけて、Bean を登録する必要があると思いますが、
Quarkus も同じ感じで @Path
をつけると DI コンテナに登録されます。
GreetingResource.java
package jp.co.persolcareer; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; @Path("/hello") public class GreetingResource { @GET @Produces(MediaType.TEXT_PLAIN) public String hello() { return "Hello from RESTEasy Reactive"; } }
一旦ここはプロジェクト作成時のままにしておきます。
Service クラスを実装する
次に Service クラスを実装してみます。GreetingService.java
を作成してみます。
単純にメッセージを返すだけです。
GreetingService.java
package jp.co.persolcareer.service; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class GreetingService { static final String message = "Good Morning!"; String getMessage() { return message; } }
DI コンテナに登録するために@ApplicationScoped
をつけています。
Spring 同様にスコープは@RequstScoped
や@SessionScoped
などあるようです。(Quarkus アプリケーションで使用できるスコープ)
Resource クラスから Service クラスを使用するコードに変更します。
@Path("/hello") public class GreetingResource { GreetingService service; @Inject GreetingResource(GreetingService service) { this.service = service; } @GET @Produces(MediaType.TEXT_PLAIN) public String hello() { return service.getMessage(); } }
@Inject
をつけてGreetingService
クラスを注入しています。@Inject
はコンストラクタが 1 つであれば省略できます。
Quarkus ではコンストラクタインジェクション、セッターインジェクション、フィールドインジェクションが使えるようです。
この辺りは Spring と同様ですね。
動作確認をしてみます。
~ $ curl -w "\n" http://localhost:8080/hello Good Morning!
正常に動いています!
DB にアクセスする処理を実装する
次は、データベースにアクセスする処理を書いてみます。
今回は postgres のコンテナを使います。
試すだけなのでデータの永続化はしていないです。
docker-compose.yml
version: '3' services: postgres: container_name: demo_db image: postgres:16 ports: - "5432:5432" volumes: - ./postgres/init:/docker-entrypoint-initdb.d environment: POSTGRES_USER: "postgres" POSTGRES_PASSWORD: "postgres" POSTGRES_DB: demo
コンテナ生成時にメッセージテーブルを作成し、メッセージ("Good Night")を一件入れるようにしておきます。
init.sql
DROP TABLE IF EXISTS message; CREATE TABLE message ( id integer NOT NULL PRIMARY KEY, body varchar(50) NOT NULL ); CREATE SEQUENCE message_id_seq START 1; INSERT INTO message (id, body) VALUES(nextval('message_id_seq'), 'Good Night.');
以下のコマンドで extension を追加します。
dodaX では MyBatis を使っているので、ここでも MyBatis を使ってみます。
quarkus ext add io.quarkus:quarkus-jdbc-postgresql quarkus ext add io.quarkiverse.mybatis:quarkus-mybatis
build.gradle の dependencies に追加されていますね。
dependencies { implementation 'io.quarkiverse.mybatis:quarkus-mybatis:2.2.0' implementation 'io.quarkus:quarkus-jdbc-postgresql' ... }
では、データベースにアクセスするコードを書いてみます。( MyBatis ドキュメント)
まずは db の接続設定から。
application.properties
quarkus.datasource.username=postgres quarkus.datasource.password=postgres quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/demo quarkus.datasource.jdbc.max-size=16
次に Entity。
Message.java
package jp.co.persolcareer.entity; public record Message(Integer id, String body) { }
次に Mapper。Spring Boot で書くときと変わりません。
MessageMapper.java
package jp.co.persolcareer.mapper; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import jp.co.persolcareer.entity.Message; @Mapper public interface MessageMapper { @Select("SELECT * FROM message WHERE id = #{id}") Message getMessageById(Integer id); @Insert("INSERT INTO message (id, body) VALUES (nextval('message_id_seq'), #{body})") Integer createMessage(@Param("body") String body); }
合わせて、Service クラスと Resource クラスを修正します。
GreetingService.java
@ApplicationScoped public class GreetingService { MessageMapper messageMapper; GreetingService(MessageMapper messageMapper) { this.messageMapper = messageMapper; } public String getMessageById(Integer id) { Message message = messageMapper.getMessageById(id); return message.body(); } }
GreetingResource.java
@Path("/{id}") @GET @Produces(MediaType.TEXT_PLAIN) public String getMessage(@PathParam("id") Integer id) { return service.getMessageById(id); } @POST public void createMessage(@FormParam("body") String body) { service.createMessage(body); }
では動作確認をしてみましょう!
~ $ curl -w "\n" http://localhost:8080/hello/1 Good Night.
POST リクエストも投げてみます!
~ $ curl -X POST -d 'body=How is it going.' http://localhost:8080/hello ~ $ curl -w "\n" http://localhost:8080/hello/2 How is it going.
普通に使えますね...!
トランザクションを使ってみる
次に、トランザクションを使ってみたいと思います。 (Quarkus でのトランザクション)
Quarkus では、Spring Boot と同様に@Transactional
をつけることで宣言的にトランザクションを管理できます。
実際に記述して動かしてみようと思います。
GreetingService.java
@Transactional public void createMessage(String body) { messageMapper.createMessage(body); if (body.equals("please throw exception.")) { throw new RuntimeException("error"); } }
プロダクションでは書かないようなコードですが、確認のためにこんな感じにしました。
まず、データベースにデータを登録してから、
受け取ったbody
がplease throw exception
である場合はRuntimeException
を投げるようにしています。
トランザクションが管理されていない場合は、例外が発生する場合でもデータベースにデータが登録されてしまいます。
では動作を確認してみましょう!
~ $ curl -X POST -d 'body=This should be commited.' http://localhost:8080/hello ~ $ curl -w "\n" http://localhost:8080/hello/3 This should be commited.
当然正常に動きますね。
では、please throw exception
を指定して例外を発生させてみます!
~ $ curl -X POST -d 'body=please throw exception.' http://localhost:8080/hello {"details":"Error id 243a3985-c04e-4937-aabc-aa4529ed262c-7, java.lang.RuntimeException: error","stack":"java.lang.RuntimeException: ...(省略)
例外が発生しました。トランザクションが管理されていれば、データはデータベースに登録されていないはずです。
確認してみましょう!
~ $ curl -w "\n" http://localhost:8080/hello/4 {"details":"Error id 243a3985-c04e-4937-aabc-aa4529ed262c-9, java.lang.NullPointerException: ","stack":"java.lang.NullPointerException
NullPointerException
ですね。ちゃんとトランザクション制御されています。
ここまで、Quarkus の実装を試してきましたが
DI・AOP をサポートしていることもあって、書き方については Spring Boot とそこまで大きく変わらない印象ですね。
Quarkus でネイティブ実行可能ファイルを生成して実行してみる
前述したように、Quarkusではネイティブ実行可能ファイルの生成がサポートされているので生成してみたいと思います。 ネイティブ実行可能ファイルの生成
GraalVM のインストール・設定
まずはGraalVMをこちらからダウンロードします。
ダウンロードしたら拡張属性の除去と解凍をし、JavaVirtualMachines
配下に移動。
~ $ sudo xattr -r -d com.apple.quarantine graalvm-jdk-21_macos-aarch64_bin.tar.gz ~ $ tar -xzf graalvm-jdk-21_macos-aarch64_bin.tar.gz ~ $ sudo mv graalvm-jdk-21.0.1+12.1 /Library/Java/JavaVirtualMachines
GRAALVM_HOME にpathを設定します。
~ $ export GRAALVM_HOME=/Library/Java/JavaVirtualMachines/graalvm-jdk-21.0.1+12.1/Contents/Home
ビルドして実行する
ネイティブ実行可能ファイルをビルドしてみます。 ビルドに 1 分ほどかかりました。
~/sandbox/quarkusdemo $ quarkus build --native BUILD SUCCESSFUL in 1m 11s 11 actionable tasks: 3 executed, 8 up-to-date
実行してみます!
~/sandbox/quarkusdemo $ ./build/quarkusdemo-1.0.0-SNAPSHOT-runner __ ____ __ _____ ___ __ ____ ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ 2023-12-05 09:35:36,583 INFO [io.quarkus] (main) quarkusdemo 1.0.0-SNAPSHOT native (powered by Quarkus 3.6.0) started in 0.032s. Listening on: http://0.0.0.0:8080 2023-12-05 09:35:36,584 INFO [io.quarkus] (main) Profile prod activated. 2023-12-05 09:35:36,584 INFO [io.quarkus] (main) Installed features: [agroal, cdi, jdbc-postgresql, mybatis, narayana-jta, resteasy-reactive, smallrye-context-propagation, vertx]
早!0.032 秒で起動しています。
しかし、起動したのは良いのですが、GETリクエストを送ってみると以下のエラーが出ました。
2023-12-05 09:36:09,127 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-1) HTTP Request to /hello/1 failed, error id: 330b3903-f8a9-4b45-bb50-976396bdaed2-1: org.apache.ibatis.exceptions.PersistenceException: ### Error querying database. Cause: org.apache.ibatis.executor.ExecutorException: No constructor found in jp.co.persolcareer.entity.Message matching [java.lang.Integer, java.lang.String] ### The error may exist in jp/co/persolcareer/mapper/MessageMapper.java (best guess) ### The error may involve jp.co.persolcareer.mapper.MessageMapper.getMessageById ### The error occurred while handling results ### SQL: SELECT * FROM message WHERE id = ? ### Cause: org.apache.ibatis.executor.ExecutorException: No constructor found in jp.co.persolcareer.entity.Message matching [java.lang.Integer, java.lang.String] at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:156) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:142) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:75) at java.base@21.0.1/java.lang.reflect.Method.invoke(Method.java:580) at io.quarkiverse.mybatis.runtime.TransactionalSqlSession$TransactionalSqlSessionInterceptor.invoke(TransactionalSqlSession.java:197)
MyBatisの処理中で、Message
クラスのコンストラクタが無い、と言われてますね。
レコードクラスを定義しているのでコンストラクタは存在するはずですが、Message
クラスがネイティブ実行可能ファイルに含まれていないようです。
というのもGraalVM では
コンパイル時にすべてのクラスを特定し、それらをネイティブイメージに含めるのですが、リフレクションなどを使用して動的に読み込まれるようなクラスはネイティブイメージに含まれません。
MyBatis は実行時にリフレクションを使って mapper のメソッドが返却する型を判断しています。
リフレクションを使うということは、動的にクラスを読み込んでいるわけです。
では、どうするか?
リフレクションを使うことを明示的設定してあげることで、リフレクションで利用するクラスについてもネイティブイメージに含めることができます!
MessageMapper.java に@RegisterForReflection
を設定します。
@Mapper @RegisterForReflection(classNames = {"jp.co.persolcareer.entity.Message"}) public interface MessageMapper { @Select("SELECT * FROM message WHERE id = #{id}") Message getMessageById(Integer id);
これでビルド、実行してみると正常に動きました!
~ $ curl -w "\n" http://localhost:8080/hello/1 Good Night.
Quarkus はネイティブビルドを全面的にサポートしようとしているので、基本的には設定不要でネイティブビルドができますが、まだサポートされていないライブラリもありそうです。
MyBatis についてはこの辺りで少し言及されていました。
Spring Boot でネイティブ実行可能ファイルを生成する
冒頭で Quarkus と Spring Boot との起動時間を比較したと思いますが、 Quarkus はネイティブ実行可能ファイルを実行し、Spring Boot は JVM 上で実行していました。
しかし、Spring Boot 3.0 からネイティブ実行可能ファイルの生成がサポートされているので、Spring Boot のネイティブビルド実行とも比較してみます。
Quarkus アプリケーションと同様に MyBatis でデータベースにアクセスする処理まで記載した Spring プロジェクトを用意しました。
そこから、build.gradle にネイティブイメージビルドするための依存関係を以下の通り記載していきます。
build.gradle
plugins { id 'org.graalvm.buildtools.native' version '0.9.28' ... } repositories { maven { url "https://oss.sonatype.org/content/repositories/snapshots" } ... } dependencies { compileOnly 'org.mybatis.spring.native:mybatis-spring-native-core:0.1.0-SNAPSHOT' ... }
また、MyBatis を使用する場合はネイティブイメージビルド用の設定クラスMyBatisNativeConfiguration.javaを作成しなければならないようです。
こちらに書かれています。
では、ネイティブコンパイルします。
~/sandbox/springdemo $ ./gradlew nativeCompile
そして実行します。
~/sandbox/springdemo $ ./build/native/nativeCompile/springdemo . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.1.0-SNAPSHOT) (省略) 2023-12-05T09:40:18.434+09:00 INFO 7066 --- [ main] c.e.springdemo.SpringdemoApplication : Started SpringdemoApplication in 0.067 seconds (process running for 0.076)
0.067 秒...。早いな...。
5 回試行の平均を Quarkus のネイティブ実行と比較してみます。
Application | StartUp Time |
---|---|
Quarkus (Native Image) | 0.023s |
Spring Boot (Native Image) | 0.072s |
Quarkus の方が早いけど、Spring Boot も十分早いですね〜。
Quarkus のエコシステム
Spring は多くのプロジェクトから構成されていて、エコシステムがかなり充実している印象ですが、
Quarkus も RedHat によってサポートされていることと、500 以上のエクステンションがサポートされています。Quarkus のプラットフォーム
今回は触り程度なのでどこまでの機能がサポートされているのか分かりませんが、セキュリティ、キャッシュあたりも触ってみたいです。
ただ、GitHub のスター数を比較してみると、Spring Boot の伸びの方が大きいですね。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/k/kamiikepersol/20231205/20231205093414.png)
新しい技術をベースにして他のフレームワークが出てきても、Spring は早い対応で新しい技術をサポートしている印象がありますね。
特に昨今はコンテナやサーバレスアーキテクチャのサポートが活発化しているような。
JVM の高速な起動を目指す CRaC をサポートする方針も打ち出しています。JVM Checkpoint Restore
まとめ
Quarkusについて調査しつつ、データを登録・取得するAPIをQuarkusを使用して実装してみました。
Quarkus にも今後注目していきたいところですが、Spring Boot も昨今のコンテナ環境をかなり意識して起動時間やフットプリントの最適化に力を入れている印象を受けたので、
フレームワーク選定となると、まだまだ Spring Boot が選ばれるのかなぁと思いました。
最後に
dodaX のエンジニアリングチームでは一緒に働く仲間を募集しています!
今回の記事はバックエンドの記事となりましたが、バックエンドだけでなく、フロントエンドエンジニア、インフラエンジニア、アーキテクトも絶賛募集中です!
興味ある方はぜひ採用サイトからご応募ください!
上池 哲平 Teppei Kamiike
doda_Xプロダクト統括 doda_Xエンジニアリンググループ エンジニア
2023年8月にパーソルキャリアに中途入社。前職では独立系SIerでWebシステムの開発に従事。現在はdodaXのバックエンドエンジニアとして、プロダクトの改善に取り組んでいる。
※2023年12月現在の情報です。