クラスメソッドメンバーズのバックエンドAPIをリプレイスしました

クラスメソッドメンバーズのバックエンドAPIをリプレイスしました

Clock Icon2024.6.18

AWS事業本部サービス開発室の佐藤です。クラスメソッドメンバーズポータルのバックエンドAPIの開発を担当しています。

はじめに

弊社ではクラスメソッドメンバーズ(以下メンバーズ)と呼ばれるAWSの利用費割引や請求代行、コンサルティング、セキュリティ、24/365サポートなどAWSに関することをおまかせしていただけるサービスを提供しています。その中でメンバーズに契約いただいたお客様に対して、クラスメソッドメンバーズポータル(以下CMP)というクラスメソッドサポートへの問い合わせやAWSアカウントリソースの利用状況を可視化できるWebサービスを提供しています。今回、CMPで利用しているバックエンドAPIをリプレイスしたのでその背景とリプレイス方法について簡単に紹介したいと思います。

リプレイスの背景

CMPは契約しているお客様向けに2013年頃から提供しているWebサービスで、バックエンドはSpring Boot + Javaで実装されています。歴史の長いサービスのためSpring Bootのフレームワークのバージョンや各種利用しているライブラリが古く継続的なアップデートができない状態になっていました。他にもCMPのフロントエンドの刷新が進んでいて、バックエンドも大幅なコード変更が必要ということも背景にありました。

既存のSpring Boot + Javaの資産をなるべく流用したかったため、リプレイス先はサーバーサイドKotlin(Spring Boot)に決まりました。

環境

旧API

ライブラリ バージョン
Java 11
Spring Boot 2.1.9
Gradle 6.9.2

新API

ライブラリ バージョン
Kotlin 1.9.23
Spring Boot 3.1.11
Gradle 7.6

リプレイスの方針

リプレイスには以下の2つの方針がありました。

  • 既存のJavaコードを徐々にKotlinに移行しつつライブラリやフレームワークのバージョンを上げていく
  • 新規でKotlinのプロジェクトを作成し、既存のJavaコードを移行していく

今回のリプレイスでは後者を選択しました。

ちょうどSpring Boot 3系のGAがアナウンスされていてSpring Boot 2系からの移行が大変そうなこと、新旧APIで並行稼働しつつ段階的に移行していく必要があるなど、既存のコードベースを修正するのではなく、新規で作成しライブラリやフレームワークも最新にしつつ実装していく方針にしました。

リプレイスの方法

新規Kotlinプロジェクトの作成

リプレイスするにあたって、まずは Spring Initializr を利用しSpring Boot 3系 + Kotlinの雛形を作成しました。必要なライブラリやバージョンを画面ポチポチで作成できるので便利です。Projectは Gradle - Kotlin、Languageは Kotlin、Spring Bootは当時最新だった 3.1.2 を選択して作成しました。作成したらzipファイルがダウンロードできるのでローカルで展開して準備完了です。

既存のJavaプロジェクトから必要なファイルをコピー

新規でプロジェクトを作成したら、既存のコードから必要なJavaファイルをコピーしていきました。基本的にはController, Request, Response, Service, DTO, Entity, Repository, ConfigなどのSpringの主要なコンポーネントをコピーしました。この段階で必要なさそうなJavaのファイルなどを棚卸しています。

JavaからKotlinへの変換

次にKotlinへの変換を行いました。変換についてはInteliJ IDEAの JavaからKotlinへの変換機能 というのがあり、基本的にはこちらを利用して変換作業を行いました。Javaのファイルを右クリックして Convert Java File to Kotlin File を選択するだけで全て自動的に変換してくれます。

例えば、以下のようなLombokを利用したJavaのEntityクラスの場合

@ToString  
@NoArgsConstructor  
@AllArgsConstructor
@EqualsAndHashCode  
@Entity
@Table(name = "hoge")
public class Hoge implements Serializable {  
    @Id  
    @Column(name = "id")  
    @Getter  
    @Setter
    private var id: Long;

    @Getter  
    @Setter    
    private String name;  
}

Kotlinのデータ変換機能を利用すると以下のようなコードに変換してくれます。

@ToString  
@NoArgsConstructor  
@AllArgsConstructor  
@EqualsAndHashCode
@Entity
@Table(name = "hoge")
class Hoge : Serializable {  
    @Id  
    @Column(name = "id")  
    @Getter  
    @Setter
    private var id: Long? = null;

    @Column(name = "name")  
    @Getter  
    @Setter    
    private val name: String? = null  
}

ただ、このままだとLombokのアノテーションが付与されているので冗長です。また、すべてNullableなプロパティに変換されてしまうという問題もあります。Kotlinの場合はdata classがあるので、それを利用する形に修正します。すると以下のようになります。

@Entity
@Table(name = "hoge")
data class Hoge(
    @Id  
    @Column(name = "id")  
    val id: Long,

    @Column(name = "name")
    val name: String,
)

JavaだとアノテーションまみれだったのがKotlinだとdata classを利用することでスッキリ書くことができました。

他にも、Kotlinは基本的にJavaの機能がそのままつかえるので、JavaのStream APIやgetterなどはそのまま変換されます。

@GetMapping("/{id}")
public Todo getTodo(@PathVariable Long id) {
    return todoList.stream()
        .filter(todo -> todo.getId().equals(id))
        .findFirst()
        .orElse(null);
}
@GetMapping("/{id}")
fun getTodo(@PathVariable id: Long?): Todo? {
    return todoList.stream()
        .filter { todo: Todo? -> todo.getId().equals(id) }
        .findFirst()
        .orElse(null)
}

このままでも問題なく動作するんですが、せっかくKotlinを利用しているのでKotlinの書き方にしたいですよね。Kotlinだと以下のように書けます。

@GetMapping("/{id}")
fun getTodo(@PathVariable id: Long): Todo {
    return todoList.filter { it.id == id }.firstOrNull()
}

JavaのOptionalを利用している場合も注意が必要です。OptionalがあるJavaコードをそのまま変換すると以下のように変換されます。

public Optional<Hoge> findOne(long id) {
    return Optional.ofNullable(new HogeDTO(hogeRepo.findOne(id)));  
}
fun findOne(id: Long): Optional<HogeDTO> {
    return Optional.ofNullable(HogeDTO(hoge))
}

KotlinはOptionalを利用しなくてもNull安全に書けるので上記のコードは以下のように変換します。

fun findOne(id: Long): HogeDTO? {  
     return hogeRepo.findByIdOrNull(id)?.let { HogeDTO(it) }
}

JavaのOptionalからKotlinのNull安全機能の変換については以下の記事が参考になります。

https://typealias.com/guides/java-optionals-and-kotlin-nulls/

大まかな変換についてはInteliJの機能におまかせして細かな部分を適宜修正する形で変換を行っていきました。他にもこの時点で不要なコードなども極力削除しました。

このようにInteliJの変換機能を使いつつ移行作業を行っていった結果、JavaのコードをすべてKotlinに変換する事ができました。また、全体のコード行数も30%程度削減できました。

今後は、Javaの旧APIとKotlinの新APIを並行稼働しつつ徐々に新APIに移行していく予定です。

まとめ

クラスメソッドメンバーズのバックエンドAPIのリプレイスについて簡単ですが紹介させていただきました。KotlinはJetBrainsが開発していることもあり、Kotlinに関するInteliJの機能が充実していて、InteliJのおかげでかなり楽をして移行ができたんじゃないかなと思います。また新規プロジェクトベースでリプレイスを進めたので、既存のAPIの影響を考えなくてよくドラスティックに修正を行えました。他にもSpring Boot 3系をベースに実装したので2系から3系へのマイグレーションを特に意識せずに行えたことも大きかったとおもいます。

今回のリプレイスでテスト環境の整備もすることができましたが、それについてはまた別の記事で紹介したいなと思っています。

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.