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. 2020年9月 パーソルキャリア株式会社に中途入社 dodaのバックエンド開発を中心に担当 Java, Spring, AWSが好き 齋藤 悠太 自己紹介 2

    今回の内容に関しまして、私たちの会社ではこうしている、こうした ほうがより良いのでは などありましたら是非Twitterでコメントいた だけますと嬉しいです。
  2. 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 主な技術スタック
  3. フロントエンドの課題 7 フレームワークを使わないフロントエンドの限界 • スタイルの変更が様々な画面に影響を及ぼす • 画面内の複雑な変化をjQuery/jsで行うことが難しい CLS指標の悪化 • クライアントサイドでレンダリング(CSR)によって画面のがたつきが発生

    • Google上での検索順位が低下する可能性があった お気に入り 修正したい画面 別の画面 お気に入り 別の画面では他のスタイルと 衝突して画面崩れを起こす この画面はうまく 表示されるが Pagespeed Insights (2021/3)
  4. サーバサイドの課題 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メソッドの呼び出し
  5. インフラの課題 9 • 変更に時間がかかる(クラウドリフトしただけ) • スケールしにくい • ミドルウェアのバージョンアップに時間がかかる • 設定がコード管理されていないため状態を追うのが難しい

    EC2 ミドルウェア v1 アプリケーション v1 EC2 ミドルウェア v2 アプリケーション v1 変更 EC2 ミドルウェア v2 アプリケーション v2 変更 状態をコード管理できておらず現在の状態を再現するのが難しい
  6. リビルドプロジェクトでの取り組み 10 フロントエンド • React/ Next.jsの導入 → コンポーネント化されることで意図しないスタイルが当たるのを防ぐ、 画面の複雑な状態の変化を簡単に対応、SSRによりCLS指標の悪化を防ぐ サーバサイド

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

    • APIのために新しくシステムを作成 • 画面と切り離されることで意図しない変更が減る → 大規模な変更(ライブラリのバージョン アップなど)をしやすい状態を作る • 負債を生まないための仕組みづくりを行う → テストを充実させて変更に強いシステムを作る インフラ • コンテナ(ECS)の利用 → イミュータブルになり、スケールや障害時の切り戻しを素早く実施 • IaC(CloudFormation)によるインフラの作成 → インフラの見える化・変更を素早く実施 ここを中心にお話しします。
  8. 新しいAPIシステムの概要図 12 AZ-a AWS Cloud ECS S3 Oracle Database ログ出力

    ALB APP sidecar Kinesis DB接続 AZ-c 上記同様のため省略 CloudWatch Solr Solr検索
  9. 取り組みについて話すこと 13 API開発の工夫 • 画面とは切り離されるようになったが、代わりにフロントエンドとの連携や使いやすいAPIを設計する ことが重要となった • 効率よくAPI設計やフロントエンドの連携を行う方法、API設計で気を付けている点をご紹介 負債を生まないための仕組みづくり •

    APIにより大規模な修正がしやすくなったが、修正後にIFの状態が同じであることの担保が必要 • 内部の状態(コードの品質やセキュリティ)についても、一定の状態を保てないと開発速度が落ちる • 負債を生まないためにテストを中心にいくつかの仕組みを作ったのでご紹介
  10. API開発の流れ 16 IF定義作成 実装 実装 OpenAPI モック生成 テスト サーバサイド フロントエンド

    認識合わせ 認識合わせ • OpenAPIが共通の指針となるため重要 • OpenAPIと実装が乖離しないようにする必要がある(フロント・サーバサイドどちらも)
  11. 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<EventResponse> index( @Parameter(description = "カテゴリID", example = "1") @RequestParam("categoryId") int categoryId) { return Collections.emptyList(); } } コントローラ /swagger-ui/index.htmlを表示
  12. 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 レスポンスのスキーマ情報
  13. フロントエンドの開発 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":"野村コ ンファレンスプラザ新宿"}]
  14. API設計で気を付けていること 21 • 命名を気をつける。 • ~flag という名前にしない • DBのカラム名をそのままレスポンスに利用しない (既にある実装をベースに移行するため意識しないと発生しやすい)

    • 後方互換性を保つ。保てないなら新しいバージョンで作る。 • 既存の仕様が良くない場合は、そのままAPIにするのではなく可能 な範囲で再定義する(詳細は後述) • 画面に依存したAPIを作らない(詳細は後述) https://www.oreilly.co.jp/books/9784873116860/ Web API: The Good Parts を参考にAPIを設計する
  15. 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
  16. API設計で気を付けていること 23 画面に依存したAPIを作らない • 特定の画面のみで利用される文言などをAPIで返さない • 複数カラムで返した方が汎用的に利用できるものは無理にAPIで加工して返さない – (例: 住所の都道府県、市区町村、ビル名などを持っている場合にそれらを結合して返すと、別の画面では都

    道府県のみ表示したいなどの要件に適用できなくなってしまう) • SEO対策のみのカラム値を返さない(返すとしても他の用途で利用可能なカラム名にする) コンテンツ タイトル <h1> <c:out value="${responseDto.title}" /> <h1> responseDto.setTitle( ResourceProperty.get( "title“)); title=タイトル 画面 jsp Java properties 元々の実装 { "title": "タイトル“ ... } よくない APIの作成例 タイトル 画面A タイトル 画面B API response タイトルが異なるので そのまま利用できない
  17. 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 マウント マウント&監視 ファイル出力
  18. 各種テストについて 27 技術負債を生まないためのテストと種類 • 静的コード解析 : コードにバグがないか、フォーマットが適切かをチェックする • 単体テスト :

    対象のクラス/メソッドに対してのテスト • インテグレーションテスト : API単位でのテスト Database 外部API APIサーバ コント ローラ サービス リポジト リ 静的コード解析/単体テスト インテグレーションテスト
  19. 静的コード解析 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;
  20. 静的コード解析 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 <xxx.v1.controller.HelloController.index(xxx.v1.controller.HelloForm)> 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): エラーの例
  21. 単体テストをしやすくする工夫 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
  22. 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. 作成されたコンテナへ通信
  23. 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の上書き コンテナの起動
  24. 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の呼び出しと アサーション
  25. 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を利用
  26. まとめ 37 • API開発の工夫 • OpenAPIを利用してサーバサイドとフロントエンドが効率よく分業できるようになった • springdoc-openapiを利用してOpenAPIを簡単に作成出来るようになった • 特定の画面に依存したAPIを作らないようにする

    • 技術負債を生まないための仕組みづくり • テストを書いてデグレを検知できる仕組みを作った • 脆弱性チェックにより問題が見つかってもすぐにバージョンアップできるようになった