圧倒的スピードと開発体験を手に入れろ!Micronautのご紹介。

こんにちは、Dev2テックリードのたけじいです。
普段はPythonで帳票サービスのバックエンド開発(FastAPI+SQLAchemy)に従事しております。

最近、社内で利用する契約管理システムの開発を任されまして、そちらで念願だったKotlinを使わせていただいています。

KotlinといえばAndroidアプリの開発で有名なプログラム言語ですが、その書きやすさとJavaとの互換性が話題となり、最近ではバックエンド開発にも広く利用されるようになりました。

社内勉強会でKotlinの布教活動(紹介)もしました。

Kotlinを布教する10分勉強会

私がバックエンド開発の技術選定をする際に重視していることは大きく5つあります。

  1. 処理性能が良いこと。
  2. 公式ドキュメントが充実して、利用者も多く盛り上がっていること。調べると情報が出てくること。
  3. 誰にでもわかりやすくシンプルなWebフレームワークであること。処理が追いやすいこと。
  4. 認証ライブラリがあること。
  5. データベース操作がしやすいライブラリがあること。

実際に選定する際に真っ先に候補として浮かんだのはKotlin版も用意されているSpringBootです。
SpringBootは利用者も多く、ドキュメントも充実しており、困ったことがあっても調べたらすぐに情報が出てくるようなJavaにおけるデファクトスタンダードフレームワークです。
私の技術選定の際のポイントもほぼ問題なく満たせるものとなっていますし、SpringBootは弊社に入社する前もかなり長い間使って慣れ親しんだものです。

ですが今回は、「知ってることを使って安全に進める」よりも「新しい技術を実際に使いながら知識や経験を積み上げたい!(弊社のビジョンは想像力とテクノロジーで外食産業に貢献する!!)」との想いから、新しいフレームワークにチャレンジすることに決めました。

採用したのは、最近盛り上がりを見せているMicronaut(マイクロノート)です。

micronaut.io

Micronautは、モダンなマイクロサービスやサーバレスアプリケーションを構築するための軽量なフレームワークで、Kotlinをサポートしています。Micronautは以下の点で優れています。

  1. 高速な起動時間と低メモリ消費
  2. 強力な依存性注入
  3. クラウドネイティブサポート
  4. 豊富なエコシステム
  5. 優れたテストサポート
  6. 非同期I/Oサポート
  7. 高度なセキュリティ機能
  8. APIファーストの設計

今回は、それぞれの利点について導入例を交えつつ解説をしていきたいと思います。

1. 高速な起動時間と低メモリ消費

  • 起動時間の短縮

Micronautはコンパイル時にAOPアスペクト指向プログラミング)や依存性注入(DependencyInjection)を行うため、リフレクションを使用せずに高速に起動します。

TechEmpower Framework Benchmarks
上記は私の大好きなフレームワークベンチマーク調査がまとまっているサイトになります。

堂々のベンチマーク(性能)154位!!

micronautは154位(126,807pt)(micronaut-graalvmに至っては135位(149,900pt))という高ランクに位置しています。
ちなみにspring(java)は381位(24,082pt)、fastapi(python)は306位(46,896pt)でした。(現チームで利用しているfastapiがspringを超えてるのは意外だし嬉しかったです)
MicronautはFastAPIにトリプルスコアに近い性能差をつけており、よだれが出るほど欲しい性能ですね。。!!

  • 低メモリ消費

ランタイムでのオーバーヘッドが少なく、メモリ使用量が低いです。これにより、クラウド環境でのコスト削減が期待できます。
Performance Comparison Between Spring Boot and Micronaut

メモリ割り当てのグラフ

SpringBoot パフォーマンス負荷テストのヒープ割り当ては 369 MB に、非ヒープは 87 MB に増加
Micronaut パフォーマンス負荷テストを実行した後、ヒープ割り当ては変更されず254MBのまま、非ヒープは 63 MB に増加
他の比較記事をいくつか見ても、概ねMicronautの方がメモリ使用量が少ないのが確認されています。
少ないメモリで速度も速いというのは、何事においても有利なことが多いですね!

2. 強力な依存性注入(DI)

MicronautでもSpringBootでよく使われているDI(Dependency Injection, 依存性注入)を用いたプログラミングが可能です。
DIとは、ソフトウェア設計のパターンの一つで、オブジェクトの依存関係をそのオブジェクト自身ではなく外部から注入する手法です。
DIを利用することで、コードの保守性、テスト性、柔軟性が向上するというメリットがあります。

MicronautでのDIの実装イメージをご紹介します。

@Controller("/hello")
@Produces(MediaType.APPLICATION_JSON)
@Validated
@RolesAllowed("ROLE_ADMIN")
class HelloController(
    // Controllerのコンストラクター引数に利用したいDaoやServiceを定義すると、
    // micronautがDIしてくれる。
    private val userDao: UserDao,
    private val userService: UserService,
    private val securityService: SecurityService
) {

    @Get("/")
    fun hello(): Map<String, String> {
        //各処理で、DIされているサービスなどを利用することができる。
        val attributes = securityService.authentication.get().attributes
        val result: Map<String, String> = mapOf("message" to "Hello, World!")
        return result
    }
}

Micronautではこの、コンストラクタ・インジェクションが推奨されています。
Micronaut Dependency Injection Types
フィールド インジェクションを使用すると、「クラスの要件を理解するのが難しくなり、フィールド インジェクションを使用してクラスをテストするときにNullPointerExceptionが発生しやすくなる」との理由からのようです。

3. クラウドネイティブサポート

AWS Lambdaサポート: MicronautはAWS Lambdaや他のサーバレスプラットフォーム向けに最適化されており、少ない設定でサーバレスアプリケーションを構築できます。
Kubernetes対応: MicronautはKubernetes環境でのデプロイに適しており、マイクロサービスのスケーラビリティを簡単に実現します。

こちらはまだ利用していないため説明を割愛させていただきますが、
弊社のプロダクトはAWS Lambdaがよく使われているため、それがサポートされているのは今後親和性が高いと考えました。
弊社のサービスの種類はどんどん増えており、サービス間連携の話もよく出てくるようになりました。
マイクロサービスにもしやすいというメリットがあるのは良いですね。

4. 豊富なエコシステム

広範なモジュール: データアクセス(HibernateJPA、MongoDB)、メッセージング(Kafka、RabbitMQ)、セキュリティ(OAuth2、JWT)、およびAPIクライアント(HTTP Client)など、多くのモジュールが提供されています。
GradleやMavenのサポート: 主なビルドツールをサポートしており、既存のプロジェクトに簡単に統合できます。

Micronautを使い始めてびっくりしたのは、その提供されているモジュール(ライブラリ)の多さです。
あまり知らなかったフレームワークだったのですが、それにしてもこんなに広範囲なモジュールが用意されているのかとびっくりしました。
このライブラリをサポートしてるかな〜?と調べると、大抵の場合、micronaut-XXというモジュールが用意されています。

https://github.com/orgs/micronaut-projects/repositories?type=all

Micronaut関連のprojectがずらっと並ぶ。その数119(2024/6現在)

githubを調べると119個もあります。すごい。

例えば、micronautでバリデーションどうやるのかな〜と探すと、これがhitしますし、
https://github.com/micronaut-projects/micronaut-validation
redis使いたいな〜となったらこれがhitします。
https://github.com/micronaut-projects/micronaut-redis

とにかく、「お前、ぽっと出の芸人じゃないな?!」という感じで、便利なライブラリ群がかなり用意されています。あまり困ることはなさそうです。
今回は、選定ポイントにも挙げたデータベースライブラリ(jOOQ)の使い方についてご紹介します。

jOOQ

www.jooq.org
Java・Kotlinでデータベースを扱う際に有名なのはSpring Data JPAですが、私は個人的にこのjOOQをお勧めしています。
jOOQのメリットは、SQLを書くようにデータベースから値を取得する処理をかけるところです。

システムは改修を重ねたりデータが増えてくると、どうしても性能問題が発生します。リレーショナルデータベースを使ったシステムの場合、やはり性能対策を取る上でSQLのチューニングは必須の対策です。
SQLの書き味と同じようにかけるjOOQは性能対策を取る時も、効果的に行えるというのが大きいメリットだと考えています。

前置きが長くなりましたが、jOOQの導入方法です。

1. build.gradle.kts
必要なライブラリを追加します。

implementation("org.jooq:jooq:3.19.8") //io.micronaut.sql:micronaut-jooqだとJooqRecordListenerを挟めないため、jooqを直接使用
implementation("org.jooq:jooq-meta:3.19.8")
implementation("org.jooq:jooq-codegen:3.19.8")
runtimeOnly("mysql:mysql-connector-java")

MicronautからもjOOQサポートライブラリが出ているのですが、調べた限りJooqRecordListener(insertやupdateの際にcreated_atやupdated_atに現在日時を入れて更新する仕組み)を挟む方法がわからなかったため、素のjooqライブラリを入れています。

2. application.ymlにデータベースに関する定義をする
datasourcesにデータベース接続に関する情報を定義します。こちらはローカルにMySQLでcontractというデータベースを立ち上げている場合の例になります。

datasources:
  default:
    url: jdbc:mysql://localhost:3306/contract
    username: contract
    password: contract

3. JooqFactoryでDSLContextをDIできるようにする
以下のように、DSLContexxtをDIできるようにFactoryを作り、@BeanアノテーションをつけてDaoなどでDSLContextをDIできるようにします。

import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import org.jooq.Configuration
import org.jooq.DSLContext
import org.jooq.SQLDialect
import org.jooq.impl.DefaultConfiguration
import javax.sql.DataSource

@Factory
class JooqFactory {

    @Bean
    fun dsl(dataSource: DataSource): DSLContext {
        val configuration = DefaultConfiguration()
        configuration.setSQLDialect(SQLDialect.MYSQL);
        configuration.set(dataSource);
        configuration.apply { set(JooqRecordListener()) }
        return configuration.dsl()
    }
}

4. DSLContextをDIして利用
これで準備が整いました。データベースにアクセスする処理を書きます。
3で用意したDSLContextをDIして処理を書きます。

以下がcustomer_planを駆動表にして、plan, basic_plan_typeをinner join、option_plan_typeをouterjoinしてSELECTする処理です。
SQLの感覚に近い記述をKotlinでできていることがわかると思います。

@Singleton
class CustomerPlanDao(private val dsl: DSLContext) {
    //conditionでcustomerを検索する
    fun getCustomerPlansByCondition(customerId: Int, condition: String?): List<CustomerPlanListRecordDto> {
        val query = dsl.select(
            CUSTOMER_PLAN.ID,
            PLAN.ID,
            CUSTOMER_PLAN.DISPLAY_NAME,
            PLAN.NAME,
            BASIC_PLAN_TYPE.NAME,
            OPTION_PLAN_TYPE.NAME)
            .from(CUSTOMER_PLAN)
            .join(PLAN).on(CUSTOMER_PLAN.PLAN_ID.eq(PLAN.ID))
            .join(BASIC_PLAN_TYPE).on(PLAN.BASIC_PLAN_TYPE_ID.eq(BASIC_PLAN_TYPE.ID))
            .leftOuterJoin(OPTION_PLAN_TYPE).on(PLAN.OPTION_PLAN_TYPE_ID.eq(OPTION_PLAN_TYPE.ID))
            .where(CUSTOMER_PLAN.CUSTOMER_ID.eq(customerId))
        condition?.let {
            query.and(CUSTOMER_PLAN.DISPLAY_NAME.like("%$condition%").or(PLAN.NAME.like("%$condition%")))
        }
        val items = query
            .orderBy(BASIC_PLAN_TYPE.ID.asc(), OPTION_PLAN_TYPE.ID.asc())
            .map { CustomerPlanListRecordDto.from(it) }
            .toList()
        return items
    }
}

5. 優れたテストサポート

統合テスト: Micronautはコンパイル時に依存関係を解決するため、テスト時の起動時間が短縮されます。
Kotestとの統合: KotlinのテストフレームワークであるKotestとシームレスに統合でき、ユニットテストや統合テストの作成が容易です。

テスト起動時間が短縮されるのは、1の高速起動を考えるとその通りで嬉しいですね。
micronautは各種testingフレームワークのサポートも当然されています。
https://micronaut-projects.github.io/micronaut-test/3.0.4/guide/index.html#kotest

その中でもKotestは以下のような利点があり、良いテストを書ける便利な機能が揃っています。
1. Kotlinに特化
2. 多様なテストスタイルのサポート
3. 強力なマッチャー
4. データ駆動テストの簡便さ
5. 優れた統合性(Spring、Micronaut、Ktor、Armeriaなどのフレームワーク)
6. 活発なコミュニティとドキュメント

もちろん、Spockや基本のjUnitなど、Kotest以外のテスティングフレームワークもサポートしていますし、慣れたものを使うこともできそうです。

6. 非同期I/Oサポート

ReactorやRxJavaのサポート: 非同期プログラミングモデルをサポートし、高パフォーマンスなアプリケーションを構築できます。
Coroutineサポート: Kotlinのコルーチンを使用したシンプルで効率的な非同期コードを書くことができます。

こちらもまだ利用していないため説明を割愛させていただきますが、非同期プログラミングは遠くない将来に使うことになるかと思います。

7. 高度なセキュリティ機能

組み込みのセキュリティ: OAuth2、JWT、LDAP、セッション管理など、多様なセキュリティ機能を提供します。
カスタマイズ可能な認証と認可: アプリケーションの要件に応じて、認証と認可の仕組みを簡単にカスタマイズできます。

今回はこの中で採用したJWT認証の導入方法についてご紹介します。

JWT認証の導入

1. build.gradle.kts
必要なライブラリを追加します。

implementation("io.micronaut.security:micronaut-security-annotations")
implementation("io.micronaut.security:micronaut-security-jwt")
implementation("org.springframework.security:spring-security-crypto")

2. application.yml
認証をかける/かけないURLパターンの定義と、JWTトークンの定義を記載します。
/auth/loginはログインAPIなので認証をかけていません。(isAnonymous()で定義)
後述しますが、開発時はswaggerを見れるようにしたいので、こちらも認証なしにしてあります。

micronaut:
  security:
    enabled: true
    # 以下で認証をかけないURLをisAnonymous()で指定。それ以外はisAuthenticated()
    intercept-url-map:
      - pattern: '/swagger-ui/**'
        httpMethod: GET
        access:
          - 'isAnonymous()'
      - pattern: '/swagger-ui'
        httpMethod: GET
        access:
          - 'isAnonymous()'
      - pattern: '/swagger/**'
        httpMethod: GET
        access:
          - 'isAnonymous()'
      - pattern: '/auth/login'
        httpMethod: POST
        access:
          - 'isAnonymous()'
      - pattern: '/**'
        access:
          - 'isAuthenticated()'
    token:
      jwt:
        enabled: true
        signatures:
          secret:
            generator:
              secret: ${JWT_GENERATOR_SIGNATURE_SECRET:JWT秘密鍵}

3. jwt認証Providerを実装
あとは、HttpRequestAuthenticationProviderを継承したjwt認証Providerを実装するだけです。
今回はuserテーブルにメールアドレスと暗号化したパスワードを保持するようにしたので、UserDaoと暗号化したパスワードを検証するBCryptPasswordEncoderServiceをDIしています。BCryptPasswordEncoderServiceは、BCryptPasswordEncoderを利用したシンプルな暗号化と入力パスワード検証メソッドを備えたサービスクラスです。

@Singleton
@Connectable
open class CustomAuthenticationProvider(private val userDao: UserDao, private val passwordEncoderService: BCryptPasswordEncoderService) :
    HttpRequestAuthenticationProvider<Any> {
    override fun authenticate(
        requestContext: HttpRequest<Any>?,
        authRequest: AuthenticationRequest<String, String>
    ): AuthenticationResponse {
        // ユーザーが存在しない場合は認証失敗
        val user = userDao.fetchByEmail(authRequest.identity).ifEmpty { return AuthenticationResponse.failure(AuthenticationFailureReason.USER_NOT_FOUND) }[0]
        if (passwordEncoderService.matches(authRequest.secret, user.passwordHash)) {
            // 認証成功したらロールも返す
            return AuthenticationResponse.success(authRequest.identity, listOf(user.roleType))
        }
        return AuthenticationResponse.failure(AuthenticationFailureReason.CREDENTIALS_DO_NOT_MATCH)
    }
}

@Singleton
open class BCryptPasswordEncoderService : PasswordEncoder {
    private var delegate: PasswordEncoder = BCryptPasswordEncoder()

    override fun encode(rawPassword: CharSequence?): String {
        return delegate.encode(rawPassword)
    }

    override fun matches(rawPassword: CharSequence?, encodedPassword: String?): Boolean {
        return delegate.matches(rawPassword, encodedPassword)
    }
}

これだけでシンプルにJWT認証を導入することができました。

8. APIファーストの設計

Swagger/OpenAPIのサポート: APIドキュメントを自動生成し、API開発の生産性を向上させます。
GraphQLのサポート: GraphQLを使用して、柔軟で効率的なAPIを構築できます。

特にSwaggerについては、実装するだけで別途Swaggerを出力するのに必要な記述しなくて良い(自動生成)のがメリットです。
今回はSwaggerの導入方法についてご紹介します。

Swagger自動生成の導入

1. build.gradle.kts
必要なライブラリを追加します。

ksp("io.micronaut.openapi:micronaut-openapi")
implementation("io.swagger.core.v3:swagger-annotations")

2. application.yml にswaggerの表示に必要な定義の記載

micronaut:
  router:
    static-resources:
      swagger:
        paths: classpath:META-INF/swagger
        mapping: /swagger/**
      swagger-ui:
        paths: classpath:META-INF/swagger/views/swagger-ui
        mapping: /swagger-ui/**

3. micronautを起動しているApplication.ktのmain部分に@OpenAPIDefinitionを定義

import io.micronaut.runtime.Micronaut
import io.swagger.v3.oas.annotations.OpenAPIDefinition
import io.swagger.v3.oas.annotations.info.Info

@OpenAPIDefinition(
    info = Info(
        title = "Hello world",
        version = "0.0"
    )
)
object Api {
}

fun main(args: Array<String>) {
    Micronaut.build()
        .args(*args)
        .start()
}

たったこれだけで、あとはAPIを通常通り実装するだけ。自動的にSwaggerページが生成されるようになります!
ローカルで起動した際は http://localhost:8080/swagger-ui でアクセスできます。

これで、簡単にAPIの実行も試せますし、フロント開発者とAPI仕様の意識合わせにも使えます。開発が捗りますね!

今回のまとめ

Micronautは、Kotlinでモダンなアプリケーションを開発するためのシンプルで性能が良い、強力なフレームワークです。

  1. 高速な起動時間と低メモリ消費
  2. 強力な依存性注入
  3. クラウドネイティブサポート
  4. 豊富なエコシステム
  5. 優れたテストサポート
  6. 非同期I/Oサポート
  7. 高度なセキュリティ機能
  8. APIファーストの設計

最初にもあげたMicronautの利点は、社内システムの開発に留まらず、ユーザ向けサービス開発についても「圧倒的スピードと開発体験」に繋がっていくと考えています。そのための足がかりとして、今後も実際の開発を通じてMicronautに慣れ親しんでいきたいと思います。


最後になりますが、弊社は開発メンバーをとてもとても募集中です。 ぜひ一緒に外食産業のためになるサービスを楽しく開発しましょう!!

xorder.notion.site