Upgrade to Pro — share decks privately, control downloads, hide ads and more …

APIによるレガシーシステムの改善

techtekt
June 05, 2023

 APIによるレガシーシステムの改善

techtekt

June 05, 2023
Tweet

More Decks by techtekt

Other Decks in Technology

Transcript

  1. APIによる
    レガシーシステムの改善
    2023/6/4 JJUG CCC 2023 Spring
    パーソルキャリア株式会社
    齋藤 悠太

    View Slide

  2. 2020年9月 パーソルキャリア株式会社に中途入社
    dodaのバックエンド開発を中心に担当
    Java, Spring, AWSが好き
    齋藤 悠太
    自己紹介
    2
    今回の内容に関しまして、私たちの会社ではこうしている、こうした
    ほうがより良いのでは などありましたら是非Twitterでコメントいた
    だけますと嬉しいです。

    View Slide

  3. アジェンダ
    3
    1. dodaについて
    2. リビルドプロジェクト
    3. API開発の工夫
    4. 技術負債を生まないための仕組みづくり

    View Slide

  4. dodaについて
    4
    • 累計会員数、約750万人の転職支援サービス
    • 10年以上運用されているシステム
    • 2018年から開発チームの内製化を開始

    View Slide

  5. リビルドプロジェクト

    View Slide

  6. dodaのシステム概要図
    6
    フロントエンド JavaScript, jQuery, Vue.js(一部画面のみ)
    サーバサイド Java, Spring, Seasar2, C#
    インフラ EC2, Apache, nginx, Tomcat, Solr, PostgreSQL, Oracle
    オンプレ
    ALB
    Java
    Java
    内部API
    C#
    内部API
    Solr
    PostgreSQL
    Oracle
    CMS
    Java
    主な技術スタック

    View Slide

  7. フロントエンドの課題
    7
    フレームワークを使わないフロントエンドの限界
    • スタイルの変更が様々な画面に影響を及ぼす
    • 画面内の複雑な変化をjQuery/jsで行うことが難しい
    CLS指標の悪化
    • クライアントサイドでレンダリング(CSR)によって画面のがたつきが発生
    • Google上での検索順位が低下する可能性があった
    お気に入り
    修正したい画面 別の画面
    お気に入り
    別の画面では他のスタイルと
    衝突して画面崩れを起こす
    この画面はうまく
    表示されるが
    Pagespeed Insights (2021/3)

    View Slide

  8. サーバサイドの課題
    8
    • テストコードが書けていない/書きにくい
    • ライブラリのバージョンアップに時間がかかる
    • 可読性が低い
    • 静的コード解析での大量のエラーやフォーマットが統一されていない
    @Controller
    @RequestMapping(“/”)
    public class HelloController extends AbstractController {
    @GetMapping
    public String index(HelloForm form) {
    this.commonValidate.convertForm(form);
    super.createLinkUrl(this.getClass().getName());
    return “home”;
    }
    }
    public abstract class AbstractController {
    @Autowired
    protected CommonValidate commonValidate;
    protected String createLinkUrl(String className) {
    if (PropConstants.PROP_URL_F101001.equals(className)) {
    return ResourceProperty.getLinkUrl(className);
    } else {
    return null;
    }
    }
    }
    コード例
    ・親クラス側でAutowiredでDIしているものを子クラス側で呼び出し
    ・親クラス側で子クラスの名前による分岐
    ・staticメソッドの呼び出し

    View Slide

  9. インフラの課題
    9
    • 変更に時間がかかる(クラウドリフトしただけ)
    • スケールしにくい
    • ミドルウェアのバージョンアップに時間がかかる
    • 設定がコード管理されていないため状態を追うのが難しい
    EC2
    ミドルウェア
    v1
    アプリケーション
    v1
    EC2
    ミドルウェア
    v2
    アプリケーション
    v1
    変更
    EC2
    ミドルウェア
    v2
    アプリケーション
    v2
    変更
    状態をコード管理できておらず現在の状態を再現するのが難しい

    View Slide

  10. リビルドプロジェクトでの取り組み
    10
    フロントエンド
    • React/ Next.jsの導入 → コンポーネント化されることで意図しないスタイルが当たるのを防ぐ、
    画面の複雑な状態の変化を簡単に対応、SSRによりCLS指標の悪化を防ぐ
    サーバサイド
    • APIのために新しくシステムを作成
    • 画面と切り離されることで意図しない変更が減る → 大規模な変更(ライブラリのバージョン
    アップなど)をしやすい状態を作る
    • 負債を生まないための仕組みづくりを行う → テストを充実させて変更に強いシステムを作る
    インフラ
    • コンテナ(ECS)の利用 → イミュータブルになり、スケールや障害時の切り戻しを素早く実施
    • IaC(CloudFormation)によるインフラの作成 → インフラの見える化・変更を素早く実施

    View Slide

  11. プロジェクトでの取り組み
    11
    フロントエンド
    • React/ Next.jsの導入 → コンポーネント化されることで意図しないスタイルが当たるのを防ぐ、
    画面の複雑な状態の変化を簡単に対応、SSRによりCLS指標の悪化を防ぐ
    サーバサイド
    • APIのために新しくシステムを作成
    • 画面と切り離されることで意図しない変更が減る → 大規模な変更(ライブラリのバージョン
    アップなど)をしやすい状態を作る
    • 負債を生まないための仕組みづくりを行う → テストを充実させて変更に強いシステムを作る
    インフラ
    • コンテナ(ECS)の利用 → イミュータブルになり、スケールや障害時の切り戻しを素早く実施
    • IaC(CloudFormation)によるインフラの作成 → インフラの見える化・変更を素早く実施
    ここを中心にお話しします。

    View Slide

  12. 新しいAPIシステムの概要図
    12
    AZ-a
    AWS Cloud
    ECS
    S3
    Oracle
    Database
    ログ出力
    ALB
    APP
    sidecar
    Kinesis
    DB接続
    AZ-c
    上記同様のため省略
    CloudWatch
    Solr
    Solr検索

    View Slide

  13. 取り組みについて話すこと
    13
    API開発の工夫
    • 画面とは切り離されるようになったが、代わりにフロントエンドとの連携や使いやすいAPIを設計する
    ことが重要となった
    • 効率よくAPI設計やフロントエンドの連携を行う方法、API設計で気を付けている点をご紹介
    負債を生まないための仕組みづくり
    • APIにより大規模な修正がしやすくなったが、修正後にIFの状態が同じであることの担保が必要
    • 内部の状態(コードの品質やセキュリティ)についても、一定の状態を保てないと開発速度が落ちる
    • 負債を生まないためにテストを中心にいくつかの仕組みを作ったのでご紹介

    View Slide

  14. API開発の工夫

    View Slide

  15. API開発の流れ
    15
    IF定義作成 実装
    実装
    OpenAPI
    モック生成
    テスト
    サーバサイド
    フロントエンド
    認識合わせ
    認識合わせ

    View Slide

  16. API開発の流れ
    16
    IF定義作成 実装
    実装
    OpenAPI
    モック生成
    テスト
    サーバサイド
    フロントエンド
    認識合わせ
    認識合わせ
    • OpenAPIが共通の指針となるため重要
    • OpenAPIと実装が乖離しないようにする必要がある(フロント・サーバサイドどちらも)

    View Slide

  17. OpenAPIの作成
    17
    springdoc-openapiでIF定義の作成
    • コントローラからOpenAPIを生成 + swagger-uiでHTMLで閲覧可能
    • 日本語の説明など追加情報を設定する
    @RestController
    @RequestMapping("/api/v1/events")
    @Tag(name = "event", description = "イベント")
    public class EventsController {
    @GetMapping
    @Operation(summary = "イベント一覧取得",
    description = "イベント情報を取得する")
    public List index(
    @Parameter(description = "カテゴリID", example = "1")
    @RequestParam("categoryId")
    int categoryId) {
    return Collections.emptyList();
    }
    }
    コントローラ /swagger-ui/index.htmlを表示

    View Slide

  18. OpenAPIの作成
    18
    springdoc-openapiでIF定義の作成
    • リクエストやレスポンスのBeanについても@Schemaで情報を付与できる
    • コードからOpenAPIを生成するため乖離が発生せず、慣れたJavaのコードで型安全にOpenAPIを
    生成できるため効率もよい。
    @Schema(description = "イベント")
    public record EventResponse(
    @Schema(title = "イベント名", example = "JJUG CCC")
    String name,
    @Schema(title = "開催日", example = "2023-06-04")
    LocalDate date,
    @Schema(title = "開催場所", example = "野村コンファレンスプラザ新宿")
    String place) {}
    Bean レスポンスのスキーマ情報

    View Slide

  19. フロントエンドの開発
    19
    OpenAPIからHTTP Client用のTypeScriptのコードを生成する
    • aspidaを利用することで型安全にHTTP通信を行うことが出来るようになる。
    • OpenAPIから型の定義がされたtsファイルを自動生成することが出来る。
    OpenAPI
    aspida
    ts
    ファイル
    const events = await apiClient.api.v1.events.$get({
    query: {
    categoryId: 1
    }
    });
    aspidaでOpenAPIから型情報を生成 型安全にHTTP通信のコードを書ける

    View Slide

  20. フロントエンドの開発
    20
    OpenAPIからモックサーバを作成する
    • Prismを利用しOpenAPIからモックサーバを作成
    • APIの開発が終わっていなくてもモックサーバを利用しフロントエンドの開発を可能にする
    OpenAPI
    Prism
    PrismでOpenAPIからモックサーバを起動
    フロントエンド
    の開発
    API呼び出し
    起動とcurlでの確認
    prism mock -p 8080 ./mockApi/api-docs.yaml
    [9:40:46] » [CLI] ... awaiting Starting Prism…
    [9:40:46] » [CLI] i info GET
    http://127.0.0.1:8080/api/v1/events?categoryId=1
    [9:40:46] » [CLI] ► start Prism is listening on http://127.0.0.1:8080
    curl http://localhost:8080/api/v1/events?categoryId=1
    [{"name":"JJUG CCC","date":"2023-06-04T00:00:00.000Z","place":"野村コ
    ンファレンスプラザ新宿"}]

    View Slide

  21. API設計で気を付けていること
    21
    • 命名を気をつける。
    • ~flag という名前にしない
    • DBのカラム名をそのままレスポンスに利用しない
    (既にある実装をベースに移行するため意識しないと発生しやすい)
    • 後方互換性を保つ。保てないなら新しいバージョンで作る。
    • 既存の仕様が良くない場合は、そのままAPIにするのではなく可能
    な範囲で再定義する(詳細は後述)
    • 画面に依存したAPIを作らない(詳細は後述)
    https://www.oreilly.co.jp/books/9784873116860/
    Web API: The Good Parts
    を参考にAPIを設計する

    View Slide

  22. API設計で気を付けていること
    22
    既存の仕様が良くない場合は、そのままAPIにするのではなく可能な範囲で再定義する
    • 10年運用されているシステムなので、途中で要件が変わる過程でおかしい仕様のものもある
    • せっかく画面とAPIを作り直すので、おかしな仕様があれば要件の再定義を可能なら実施する
    • スケジュールもあるので可能な範囲を見極めて実施
    一覧画面
    既存仕様がよくない例
    明細1
    明細2を削除
    明細2
    明細3
    No 内容
    1 明細1
    2 明細2
    2 明細3
    並び順に利用するNoを
    手動で更新
    一覧画面
    明細1
    明細3
    No 内容
    1 明細1
    2 明細3
    3 明細4
    3 明細5
    内容
    明細4
    内容
    明細5
    同時にinsertするとNoが重複
    明細4
    明細5
    Noが3のものが
    複数作成されてしまう
    可能性がある
    No.3 -> No.2
    にupdate

    View Slide

  23. API設計で気を付けていること
    23
    画面に依存したAPIを作らない
    • 特定の画面のみで利用される文言などをAPIで返さない
    • 複数カラムで返した方が汎用的に利用できるものは無理にAPIで加工して返さない
    – (例: 住所の都道府県、市区町村、ビル名などを持っている場合にそれらを結合して返すと、別の画面では都
    道府県のみ表示したいなどの要件に適用できなくなってしまう)
    • SEO対策のみのカラム値を返さない(返すとしても他の用途で利用可能なカラム名にする)
    コンテンツ
    タイトル



    responseDto.setTitle(
    ResourceProperty.get( "title“));
    title=タイトル
    画面 jsp
    Java
    properties
    元々の実装
    {
    "title": "タイトル“
    ...
    }
    よくない
    APIの作成例
    タイトル
    画面A
    タイトル
    画面B
    API response
    タイトルが異なるので
    そのまま利用できない

    View Slide

  24. 技術負債を生まないための仕組みづくり

    View Slide

  25. コンテナ(ECS)の利用
    25
    イミュータブルなリソース管理
    • AutoScalingによりコスト削減やアプリが落ちても自動復旧
    • Blue/Greenデプロイにより問題発生時に一瞬で切り戻す
    脆弱性へ即時に対応
    • ECRのイメージスキャンやAmazon Inspectorでイメージの脆弱性チェック
    • Dockerfileを変更するだけでバージョンアップ可能
    Blue/Greenデプロイ
    コンテナイメージの脆弱性診断レポート

    View Slide

  26. ECSでJavaアプリケーションを動かす際に発生した課題
    26
    EFSが使えない状況でのJFR/HeapDump出力
    • ジャンボフレームの影響でFargate1.4が利用できなかった。
    Fargate1.4.0でないとEFSも利用できない
    • サイドカーでinotifyを利用しJFRやHeapDumpの出力先をマウン
    トし、ファイルが配置されたらs3に出力するように設定
    “containerDefinitions”: [
    {
    “mountPoints” : [
    {
    “sourceVolume” : “jvmlogs_volume”,
    “containerPath” : “/logs/jvmlogs/”
    }
    ],
    “name”: “app”,
    // 省略
    },
    {
    "mountPoints" : [
    {
    "sourceVolume" : "jvmlogs_volume",
    "containerPath" : "/logs/jvmlogs/"
    }
    ],
    "name": "inotify",
    // 省略
    }
    ]
    taskdef.json
    Java
    inotify
    s3
    ECS Task
    /logs
    マウント
    マウント&監視
    ファイル出力

    View Slide

  27. 各種テストについて
    27
    技術負債を生まないためのテストと種類
    • 静的コード解析 : コードにバグがないか、フォーマットが適切かをチェックする
    • 単体テスト : 対象のクラス/メソッドに対してのテスト
    • インテグレーションテスト : API単位でのテスト
    Database
    外部API
    APIサーバ
    コント
    ローラ
    サービス
    リポジト

    静的コード解析/単体テスト インテグレーションテスト

    View Slide

  28. 静的コード解析
    28
    ビルド時に静的コード解析を実行
    • CIで実行しエラー発生時にはデプロイさせない
    • 現在はコード解析はSpotBugsで, フォーマッタはSpotless(google-java-format)で実施
    SpotBugsのエラー例 Spotlessのエラー例
    FAILURE: Build failed with an exception.
    * What went wrong:
    Execution failed for task ':spotlessJavaCheck'.
    > The following files had format violations:
    src¥main¥java¥xx¥v1¥controller¥AbstractController.java
    @@ -3,14 +3,13 @@
    import·org.springframework.beans.factory.annotation.Autowired;
    public·abstract·class·AbstractController·{
    -····@Autowired
    -····protected·CommonValidate·commonValidate;
    +··@Autowired·protected·CommonValidate·commonValidate;

    View Slide

  29. 静的コード解析
    29
    ArchUnitによるアーキテクチャテスト
    • ArchUnitを利用してプロジェクトのルールを守らないコードがある場合テストでエラーにする
    • 以下作成したルールの例
    • 依存関係などのパッケージルール(ポピュラーなArchUnitの使い方)
    • ライブラリの使い方に関するルール
    • Springに関するルール(@Transactionを付与してよい場所の指定など)
    • OpenAPIの入力必須化(springdoc-openapiのアノテーション付与必須)
    @ArchTest
    void Transactionアノテーションはinteractorにのみ設定可能() {
    methods()
    .that()
    .areDeclaredInClassesThat()
    .resideOutsideOfPackage(“XXX.interactor..")
    .should()
    .notBeAnnotatedWith(Transactional.class);
    }
    サンプルのテストコード
    Architecture Violation [Priority: MEDIUM] - Rule 'methods that are declared in
    classes that reside outside of package xxxx.interactor..' should not be annotated
    with @Transactional' was violated (1 times):
    Method is
    annotated with @Transactional in (HelloController.java:18)
    java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'methods
    that are declared in classes that reside outside of package ‘xxx.interactor..' should
    not be annotated with @Transactional' was violated (1 times):
    エラーの例

    View Slide

  30. 単体テストをしやすくする工夫
    30
    staticなメソッドの呼び出しをDIに置き換える
    • 日付取得やログ出力処理などのstaticなメソッドの呼び出しはテストが面倒になる。
    • Bean化してDIすることでテストをしやすくする
    @Component
    public class LocalDateTimeService {
    public LocalDateTime now() {
    return LocalDateTime.now();
    }
    }
    public class JobInteractor {
    private JobRepository jobRepository;
    private LocalDateTimeService localDateTimeService;
    public JobInteractor(
    JobRepository jobRepository,
    LocalDateTimeService localDateTimeService) {
    this.jobRepository = jobRepository;
    this.localDateTimeService = localDateTimeService;
    }
    public boolean isPublic(String id) {
    Job job = jobRepository.find(id).get();
    // 公開終了日時 > 現在日時
    return job.getPublicationEndDate().isAfter(
    localDateTimeService.now());
    }
    }
    @Test
    @DisplayName("公開終了日時 > 現在日時 のときtrue")
    void 公開終了日時の前ならtrueを返す() {
    String id = "123";
    Job job = new Job();
    job.setPublicationEndDate(
    LocalDateTime.of(2023, 6, 4, 16, 00, 01));
    given(jobRepository.find(id)).willReturn(job);
    given(localDateTimeService.now())
    .willReturn(LocalDateTime.of(2023, 6, 4, 16, 00, 00));
    boolean actual = jobInteractor.isPublic(id);
    assertTrue(actual);
    }
    テスト対象コード テストコード
    LocalDateTimeのBean

    View Slide

  31. APIのインテグレーションテスト
    31
    全体像
    プロダクションコード
    コント
    ローラ
    サービス
    リポジト

    APIごと
    のテスト
    コード
    4. @SpringBootTestで
    Springの起動
    モック用
    のコード
    5. Beanを置き換え
    TestContainers
    WireMock
    Flyway
    インテグレーションコード
    applicati
    on.yaml
    8. TestWebClientで
    API呼び出しと
    アサーション
    1. TestContainers実行 2. コンテナ起動
    3. application.yamlの上書き
    6. Flyway実行
    7. マイグレーション
    9. 作成されたコンテナへ通信

    View Slide

  32. APIのインテグレーションテスト
    32
    通信先インスタンスの作成
    • DBや外部APIなどの各種通信先はTestContainersを利用してDockerコンテナを実行します。
    • 外部APIはWireMockを1つ起動してエンドポイントで振り分け
    public abstract class AbstractContainerBaseTest {
    static final PostgreSQLContainer postgres;
    static {
    postgres = new PostgreSQLContainer<>(DockerImageName.parse(“postgres:12”));
    postgres.start();
    }
    // application.ymlの設定を起動したコンテナ情報で上書きする
    @DynamicPropertySource
    static void setup(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgres::getJdbcUrl);
    registry.add("spring.datasource.username", postgres::getUsername);
    registry.add("spring.datasource.password", postgres::getPassword);
    }
    }
    build.graldeでのソースセット作成
    applicati
    on.yaml
    application.yamlの上書き
    コンテナの起動

    View Slide

  33. APIのインテグレーションテスト
    33
    DBのマイグレーション
    • テスト用のDBマイグレーションにはFlywayを利用
    • テストで使う可能性のある初期データも事前にFlywayでinsertする(例えばユーザー情報など各テ
    ストで用意するのは大変なもの)
    resourcesに配置したマイグレーションファイル
    Flyway
    Create
    Table
    SQL
    Insert
    SQL

    View Slide

  34. APIのインテグレーションテスト
    34
    Springの起動~APIの呼び出しテスト
    • @SpringBootTest を付けることで実際にSpringを起動しテストが実行できる
    • インテグレーションテストではAPIの呼び出し~DBや外部APIの呼び出しまでを通しでテストする
    • WebTestClientを利用することでAPI呼び出しのメソッドチェーンでそのままアサーション
    @SpringBootTest
    public class EventIntegrationTest {
    @Autowired private WebTestClient webTestClient;
    @Test
    void データが取得可能() {
    webTestClient
    .get()
    .uri("/api/v1/events")
    .accept(MediaType.APPLICATION_JSON)
    .exchange()
    .expectStatus().isOk()
    .expectHeader().contentType(MediaType.APPLICATION_JSON)
    .expectBody().jsonPath("$.id").isEqualTo(1);
    }
    プロダクションコード
    コント
    ローラ
    サービス
    リポジト

    アプリケーション
    の起動
    APIの呼び出しと
    アサーション

    View Slide

  35. APIのインテグレーションテスト
    35
    テスト用Beanの作成
    • dodaではDBにOracleを利用しているが、コンテナでの利用に課題がある(起動が遅いなど)ため
    PostgreSQLを代わりに利用している。一部Oracle独自の関数を利用しているものもあるためそう
    いった箇所はBeanを利用してモック化している。
    • 既存のコードではテストしにくいもの(例えば共通エラー系のテスト)をテストするために
    Controllerをテスト用に作成してテストを実施
    @Dao
    @ConfigAutowireable
    public interface FunctionDao {
    @Select
    @Sql("select now()")
    LocalDate getDate();
    }
    @Dao
    @ConfigAutowireable
    public interface FunctionDao {
    @Select
    @Sql("select sysdate from dual")
    LocalDate getDate();
    }
    本来のコード テスト用に置き換え
    補足情報:Oracleのコンテナはgvenzl/oracle-xeを利用すると起動が速いようです
    プロダクションコード
    リポジト

    Dao
    Dao
    テストコード
    テスト用
    Beanを利用

    View Slide

  36. ライブラリの脆弱性チェック
    36
    ライブラリの脆弱性を定期診断する
    • OWASP Dependency-Checkのプラグインを利用することでライブラリの脆弱性をチェック
    • 日次でdependencyCheckのタスクを実行するようなパイプラインを作成
    • 一定以上のCVSSスコアのものを検知した場合は、スコアに応じてスケジューリングし対応
    • テストを充実させたことでバージョンアップを安心して実施できるようになった
    脆弱性チェックで検知した例

    View Slide

  37. まとめ
    37
    • API開発の工夫
    • OpenAPIを利用してサーバサイドとフロントエンドが効率よく分業できるようになった
    • springdoc-openapiを利用してOpenAPIを簡単に作成出来るようになった
    • 特定の画面に依存したAPIを作らないようにする
    • 技術負債を生まないための仕組みづくり
    • テストを書いてデグレを検知できる仕組みを作った
    • 脆弱性チェックにより問題が見つかってもすぐにバージョンアップできるようになった

    View Slide

  38. ご清聴ありがとうございました
    38

    View Slide