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

例外設計について考えて Kotlin(Spring Boot&Arrow)で実践する/thin...

Msksgm
June 21, 2024

例外設計について考えて Kotlin(Spring Boot&Arrow)で実践する/thinking exception design and implementation by kotlin

Kotlin Fest 2024 の「例外設計について考えて Kotlin(Spring Boot&Arrow)で実践する」の発表資料です。
https://fortee.jp/kotlin-fest-2024/proposal/c7b68f48-1209-4f63-a111-18d1b185e109

アーカイブのリンクは以下になります。

https://www.youtube.com/watch?v=XxqGVhtIZoY

Msksgm

June 21, 2024
Tweet

More Decks by Msksgm

Other Decks in Technology

Transcript

  1. はじめに ⾃⼰紹介 • 名前 • 杉本将来(HN: Msksgm) • SNS •

    X: @msksgm • GitHub: Msksgm • Zenn: msk • 「ハンズオンで学ぶサーバーサイド Kotlin」を作成(200 like 突破 🎉) • 業務 • Yahoo! オークション・Yahoo! フリマ の課⾦システムの開発保守 • Yahoo! オークション・Yahoo! フリマ の SRE 業務 3 SNS のアイコン ハンズオンで学ぶ サーバーサイド Kotlin
  2. はじめに 「例外設計について考えて Kotlin で実践する」の⽬的 • 例外設計を考えることで、プロダクト開発へ好影響を与えられるようになりたい • プロダクト開発 ≒ 企画・開発・運⽤・フィードバック、DevOps、プロダクトマネジメント、

    アジャイル(スクラム、XP)、etc... 4 企画 ・ 要件定義 ・ ユースケースの定義 ・ SLO の合意 開発 ・ 例外を実装 ・ ログ 運⽤ ・ アラート設計 ・ SLO ・ バーンアラート ・アラート ・評価 フィードバック
  3. はじめに 注意事項 • 取り扱わないこと • Spring Boot の初歩的な内容 • ドメイン駆動設計の詳細

    • Arrow-kt の詳細な利⽤⽅法 • Kotlin・Java の Exception と Error 型の定義と異なる⾔葉の使い⽅ • 「例外」という⾔葉を多⽤しますが、Kotlin・Java の Exception(例外)・Error (エラー)ではないことに注意してください 5
  4. 例外設計を考える背景 「例外設計を考える背景」の⽬的 • 例外設計が、企画・開発・運⽤・フィードバックに影響を与えること学ぶ • なぜ例外を考えるか、下図に従って概要を把握してもらう。詳細は後述の節で解説 7 企画 ・ 要件定義

    ・ ユースケースの定義 ・ SLO の合意 開発 ・ 例外を実装 ・ ログ 運⽤ ・ アラート設計 ・ SLO ・ バーンアラート ・アラート ・評価 「例外設計を考える背景」の範囲 フィードバック
  5. 例外設計を考える背景 まとめ • 例外設計はプロダクト開発の⼀連に影響を与える • それぞれのフェーズに対して好影響を与えることができる 12 企画 ・ 要件定義

    ・ ユースケースの定義 ・ SLO の合意 開発 ・ 例外を実装 ・ ログ 運⽤ ・ アラート設計 ・ SLO ・ バーンアラート ・アラート ・評価 フィードバック
  6. 「プログラマが知るべき 97 のこと」 • タイトル通りのプログラマが知るべきエッセイ集 • 通称「きのこ」(プログラマが知るべき 97 のこと) •

    邦訳だと 97 エッセイ(78 ⼈) + 10 エッセイ(⽇本⼈エンジニア 8 ⼈) • 本発表では、「技術的例外とビジネス例外を明確に区別する」を⾔及 技術的例外・ビジネス例外と予期する例外・予期しない例外 18 Dan Bergh Johnsson (出典: https://ndc-security.com/speakers/dan-bergh-johnsson ) プログラマが知るべき 97 のこと
  7. 技術的例外・ビジネス例外と予期する例外・予期しない例外 きのこ21「技術的例外とビジネス例外を明確に区別する」 の要約 「両者を同じ例外階層構造で扱ってはいけない」 19 技術的例外 ビジネス例外 発⽣起因 「契約」の外(契約外) 「契約」の⼀部(契約内)

    ビジネスロジック No Yes 具体例 DB が応答しない 引き落とそうとしたが、 残⾼が⾜りない try/catch の有無 しない(⼤域脱出) する ハンドリング フレームワークが対応する クライアントが対応する 表: 技術的例外とビジネス例外の⽐較
  8. 「セキュア・バイ・デザイン」第 9 章(⼀部)の要約 2 つを混合させることはアプリケーションに複雑さをもたらす 技術的例外・ビジネス例外と予期する例外・予期しない例外 21 技術的例外 ビジネス例外 発⽣起因

    ドメインとは無関係 技術的 or フレームワークのエラー ドメインの観点から不正 ドメイン・ルール違反 具体例 DB が応答しない 引き落とそうとしたが、 残⾼が⾜りない 表: 技術的例外とビジネス例外の⽐較 p.332 引⽤ > 開発者の中には、 例外をスローする第⼀の⽬的は、技術的なのかどうなのかにかかわらず、不正なアクションを起こさせないことであると考えており、 そのため、ビジネス例外も技術的例外も同じよう にまとめて扱う設計を好む⼈もいます。 > もしかしたら、これら 2 種類の例外の違いは⼤したことではないように思っている開発者もいるかもしれません。 > しかしながら、異なる種類の例外を同じように扱うことは、アプリケーションに様々な複雑さとセキュリティの問題をもたらすことになります。
  9. 「セキュア・バイ・デザイン」第 9 章(⼀部)の具体例(1/4) 技術的例外・ビジネス例外と予期する例外・予期しない例外 22 リスト 9.2. 同じ型を使ってビジネス例外と技術例外の両⽅を表現している例 public class

    AccountRepository { private final AccountDatabase accountDatabase; public Account fetchAccountFor( final Customer customer, final AccountNumber accountNumber ) { try { return accountDatabase .selectAccountsFor(customer) // 指定した顧客の銀⾏⼝座の取得 .stream() .filter( // 指定した⼝座番号と⼀致する銀⾏⼝座のみを選択 account -> account.number().equals(accountNumber)) .findFirst() // 銀⾏⼝座の取得 .orElseThrow( // 該当する銀⾏⼝座がない場合は例外をスロー () -> new IllegalStateException( String.format("〇〇様の銀⾏⼝座(〇〇)は⾒つかりませんでした")); ) } catch (SQLException e) { throw new IllegalStateException( String.format("〇〇様の銀⾏⼝座(〇〇)が取得できませんでした"), e); } } }
  10. 「セキュア・バイ・デザイン」第 9 章(⼀部)の具体例(2/4) 技術的例外・ビジネス例外と予期する例外・予期しない例外 23 リスト 9.2. 同じ型を使ってビジネス例外と技術例外の両⽅を表現している例 ビジネス例外 技術的例外

    public class AccountRepository { private final AccountDatabase accountDatabase; public Account fetchAccountFor( final Customer customer, final AccountNumber accountNumber ) { try { return accountDatabase .selectAccountsFor(customer) // 指定した顧客の銀⾏⼝座の取得 .stream() .filter( // 指定した⼝座番号と⼀致する銀⾏⼝座のみを選択 account -> account.number().equals(accountNumber)) .findFirst() // 銀⾏⼝座の取得 .orElseThrow( // 該当する銀⾏⼝座がない場合は例外をスロー () -> new IllegalStateException( String.format("〇〇様の銀⾏⼝座(〇〇)は⾒つかりませんでした")); ) } catch (SQLException e) { throw new IllegalStateException( String.format("〇〇様の銀⾏⼝座(〇〇)が取得できませんでした"), e); } } }
  11. 「セキュア・バイ・デザイン」第 9 章(⼀部)の具体例(3/4) 技術的例外・ビジネス例外と予期する例外・予期しない例外 24 public class AccountRepository { private

    final AccountDatabase accountDatabase; public Account fetchAccountFor( final Customer customer, final AccountNumber accountNumber ) { try { return accountDatabase .selectAccountsFor(customer) // 指定した顧客の銀⾏⼝座の取得 .stream() .filter( // 指定した⼝座番号と⼀致する銀⾏⼝座のみを選択 account -> account.number().equals(accountNumber)) .findFirst() // 銀⾏⼝座の取得 .orElseThrow( // 該当する銀⾏⼝座がない場合は例外をスロー () -> new IllegalStateException( String.format("〇〇様の銀⾏⼝座(〇〇)は⾒つかりませんでした")); ) } catch (SQLException e) { throw new IllegalStateException( String.format("〇〇様の銀⾏⼝座(〇〇)が取得できませんでした"), e); } } } リスト 9.2. 同じ型を使ってビジネス例外と技術例外の両⽅を表現している例 ビジネス例外 技術的例外 技術的例外 ビジネス例外 発⽣起因 ドメインとは無関係 技術的 or フレームワークのエラー ドメインの観点から不正 ドメイン・ルール違反 具体例 メモリ不⾜ 引き落とそうとしたが、 残⾼が⾜りない
  12. 「セキュア・バイ・デザイン」第 9 章(⼀部)の具体例(4/4) 技術的例外・ビジネス例外と予期する例外・予期しない例外 25 public class AccountRepository { private

    final AccountDatabase accountDatabase; public Account fetchAccountFor( final Customer customer, final AccountNumber accountNumber ) { try { return accountDatabase .selectAccountsFor(customer) // 指定した顧客の銀⾏⼝座の取得 .stream() .filter( // 指定した⼝座番号と⼀致する銀⾏⼝座のみを選択 account -> account.number().equals(accountNumber)) .findFirst() // 銀⾏⼝座の取得 .orElseThrow( // 該当する銀⾏⼝座がない場合は例外をスロー () -> new IllegalStateException( String.format("〇〇様の銀⾏⼝座(〇〇)は⾒つかりませんでした")); ) } catch (SQLException e) { throw new IllegalStateException( String.format("〇〇様の銀⾏⼝座(〇〇)が取得できませんでした"), e); } } } リスト 9.2. 同じ型を使ってビジネス例外と技術例外の両⽅を表現している例 ビジネス例外 技術的例外 技術的例外 ビジネス例外 発⽣起因 ドメインとは無関係 技術的 or フレームワークのエラー ドメインの観点から不正 ドメイン・ルール違反 具体例 ⽀払い済みの注⽂に更に商品を追加 メモリ不⾜ 異なる種類の例外を同じように扱うことは、 アプリケーションに様々な複雑さとセキュリティの問題 をもたらすことになります。
  13. 「技術的例外とビジネス例外」についてまとめ 技術的例外とビジネス例外を定義することでやりたいこと • 両者の例外階層構造を区別する • アプリケーションから複雑さを取り除く 技術的例外・ビジネス例外と予期する例外・予期しない例外 26 技術的例外 ビジネス例外

    発⽣起因: 契約 「契約」外 「契約」の⼀部 発⽣起因: ドメイン ドメインとは無関係 ドメインルール ビジネスロジック No Yes try/catch の有無 しない(⼤域脱出) する ハンドリング フレームワークが対応する クライアントが対応する 表: 「技術的例外とビジネス例外」まとめ
  14. 「技術的例外とビジネス例外」の経験的な限界 「技術的例外とビジネス例外」の分類で以下のようなパターンがあった • 技術的例外でクライアントがハンドリングしたいとき • ex: 排他処理でないアプリケーションにおける Insert 時の⼀意制約違反 •

    ビジネス例外でフレームワークがハンドリング(⼤域脱出)したいとき • ex: ビジネス的に存在しないマスターの組み合わせ 技術的例外・ビジネス例外と予期する例外・予期しない例外 27 「技術的例外とビジネス例外」を⾒直す必要性
  15. 技術的例外・ビジネス例外と予期する例外・予期しない例外 「技術的例外とビジネス例外」の拡張 「予期する例外・予期しない例外」という考え⽅(例外設計(kawasima)) 28 > (⾔語ごとの例外の分類を表にまとめた後の⽂章) > 各⾔語によって違いはあるが、ここでは、Expected but Unacceptedを「予期する例外」

    > Unexpectedを「予期しない例外」と呼ぶ。ここで「予期する」かどうかはプログラム上で明⽰的に > 具象型の例外をキャッチしハンドリングするかどうかを指し、⼈間が予想しうるかどうかではない。 予期する 予期しない try/catch の有無 する しない(⼤域脱出) ハンドリング クライアントが対応する フレームワークが対応する 表:「予期する例外・予期しない例外」のハンドリングの分類 出典: scrapbox, kawashima, 例外設計, https://scrapbox.io/kawasima/%E4%BE%8B%E5%A4%96%E8%A8%AD%E8%A8%88
  16. 「技術的例外とビジネス例外」と「予期する例外・予期しない例外」 技術的例外・ビジネス例外と予期する例外・予期しない例外 29 予期する 技術的例外 予期しない 技術的例外 予期する ビジネス例外 予期しない

    ビジネス例外 発⽣起因: 契約 「契約」外 「契約」外 「契約」の⼀部 「契約」の⼀部 発⽣起因: ドメイン ドメインとは 無関係 ドメインとは無関係 ドメインルール ドメインルール ビジネスロジック No No Yes Yes try/catch の有 無 する しない (⼤域脱出) する しない (⼤域脱出) ハンドリング クライアント側に 組み込む フレームワークが対応する クライアント側に 組み込む フレームワークが 対応する 具体例 ⾮同期処理によって発 ⽣しうる⼀意制約違反 外部 API の想定していな いステータスコード バリデーションエラー マスターテーブルに存在 しない組み合わせ 表: 「技術的例外とビジネス例外」・「予期する例外と予期しない例外」の組み合わせ
  17. 「技術的例外とビジネス例外」と「予期する例外・予期しない例外」 技術的例外・ビジネス例外と予期する例外・予期しない例外 30 予期する 技術的例外 予期しない 技術的例外 予期する ビジネス例外 予期しない

    ビジネス例外 発⽣起因: 契約 「契約」外 「契約」外 「契約」の⼀部 「契約」の⼀部 発⽣起因: ドメイン ドメインとは 無関係 ドメインとは無関係 ドメインルール ドメインルール ビジネスロジック No No Yes Yes try/catch の有 無 する しない (⼤域脱出) する しない (⼤域脱出) ハンドリング クライアント側に 組み込む フレームワークが対応する クライアント側に 組み込む フレームワークが 対応する 具体例 ⾮同期処理によって発 ⽣しうる⼀意制約違反 外部 API の想定していな いステータスコード バリデーションエラー マスターテーブルに存在 しない組み合わせ 表: 「技術的例外とビジネス例外」・「予期する例外と予期しない例外」まとめ 拡張部分 • 技術的例外にもハンドリングする例外が存在する • ビジネス例外にもハンドリングしない例外が存在する • ハンドリングの有無によらず、技術的例外とビジネス例外は区別する
  18. 例外の分類まとめ 技術的例外・ビジネス例外と予期する例外・予期しない例外 32 技術的例外 ビジネス例外 予期する例外 予期しない例外 予期する 技術的例外 予期しない

    技術的例外 予期しない ビジネス例外 予期する ビジネス例外 アプリケーションに複雑さをもたらさないために、技術的例外とビジネス例外を分類する 予期する例外・予期しない例外 によって、ハンドリング⽅法を分ける
  19. 「エリック・エヴァンスのドメイン駆動設計」について • 通称エヴァンス本 • ドメイン駆動設計(以下、DDD)の考え⽅は後々の設計について⼤きな影響を与えた • DDD そのものを解説した書籍 • 「実践ドメイン駆動設計」(通称、IDDD

    本) • 「ドメイン駆動設計 モデリング/実装ガイド」 • 「ドメイン駆動設計⼊⾨」 • etc... • DDD の概念を輸⼊したり独⾃拡張したりした本(「隠れ DDD 本」と個⼈的に呼んでいる) • 「セキュア・バイ・デザイン」 • 「単体テストの考え⽅/使い⽅」 • 「現場で役⽴つシステム設計の原則」 • etc.. • 「エリック・エヴァンスのドメイン駆動設計」を参考⽂献に加えている書籍 • 「ソフトウェアアーキテクチャの基礎」 • 「Design It!」 • etc... 例外設計とモデリング 39
  20. DDD の⽂化(個⼈的な解釈) • ドメインの認識と技術の側⾯から相互フィードバックで⽬標を達成する • 認識=戦略的 DDD 、技術=戦術的 DDD と呼ばれたりする

    例外設計とモデリング 40 戦略的 DDD 戦術的 DDD ユビキタス⾔語 ドメインモデリング アーキテクチャ デザインパターン 図: 戦略的 DDDと戦術的 DDD ドキュメンテーション
  21. 戦略的 DDD 戦術的 DDD ユビキタス⾔語 ドメインモデリング アーキテクチャ デザインパターン 図: 戦略的

    DDD と戦術的 DDD DDD と例外設計 • 例外はドメインに従う • 先述した例外の分類はステークホルダーで定義する必要がある • 例外をドメインに従わせるには、戦略的と戦術的の両⽅の側⾯が必要 • 最初から分類を断定できないので、学びを反映しやすいコード・⽂化が必要 例外設計とモデリング 41 例外を • ユースケースとして理解する • ビジネス側と合意する • ドキュメントに反映する ドキュメンテーション 例外を • ドメインオブジェクトとして扱う • ハンドリングする
  22. 例外設計とモデリング(1/5) 例外設計とモデリング 44 企画 ・ 要件定義 ・ ユースケースの定義 ・ SLO

    の合意 開発 ・ 例外を実装 ・ ログ 運⽤ ・ アラート設計 ・ SLO ・ バーンアラート フィードバック • アラート ビジネス ︓XXX が YYY のときには、ZZZ になります エンジニア︓AAA、BBB、CCC の時にはどうなりまか︖ ビジネス ︓AAA のときは、aaa、BBB のときは、bbb です。CCC のときには考えなくて良いです。 エンジニア︓承知しました
  23. 例外設計とモデリング(2/5) 例外設計とモデリング 45 企画 ・ 要件定義 ・ ユースケースの定義 ・ SLO

    の合意 開発 ・ 例外を実装 ・ ログ 運⽤ ・ アラート設計 ・ SLO ・ バーンアラート フィードバック • アラート エンジニア︓ - YYY のとき ZZZ - AAA のときは、aaa - BBB のときは、bbb YYY、AAA、BBB 以外のときには、例外を throw してアラートを流す
  24. 例外設計とモデリング(3/5) 例外設計とモデリング 46 企画 ・ 要件定義 ・ ユースケースの定義 ・ SLO

    の合意 開発 ・ 例外を実装 ・ ログ 運⽤ ・ アラート設計 ・ SLO ・ バーンアラート フィードバック • アラート 「アラートが発⽣︕︕︕」 - CCC のパターンが存在した - オンコールで対応した
  25. 例外設計とモデリング(4/5) 例外設計とモデリング 47 企画 ・ 要件定義 ・ ユースケースの定義 ・ SLO

    の合意 開発 ・ 例外を実装 ・ ログ 運⽤ ・ アラート設計 ・ SLO ・ バーンアラート フィードバック • アラート ビジネス: 把握できていないパターンがあった エンジニア︓ CCCには、別パターン(CCCʻ、 CCCʼʼ)があった アラート設計は、リアルタイムで対応す るのは不要だから、warn にしておこう 今回のパターンはポストモーテムにして 社内で振り返ろう
  26. 例外設計とモデリング(5/5) 例外設計とモデリング 48 企画 ・ 要件定義 ・ ユースケースの定義 ・ SLO

    の合意 開発 ・ 例外を実装 ・ ログ 運⽤ ・ アラート設計 ・ SLO ・ バーンアラート フィードバック • アラート 実際に綺麗に整理できる状況はなかなかない しかし、予期しない技術的・ビジネス例外を把握できない、例外として throw しない、 アラートが来ない、異常事態に気が付かないまま運⽤されるのは深刻な問題 例外を把握しフィードバックまで活かせる⽂化作りが⼤切
  27. 「オニオンアーキテクチャ」と例外 • 各層の役割(Spring Boot による簡易版) 例外設計とモデリング 51 Domain 層 UseCase

    層 Infrastructure 層 Presentation 層 Presentation 層 - @RestController アノテーション - エンドポイントの定義 - リクエスト/レスポンス UseCase 層 - @Service アノテーション - ドメインオブジェクトを組み合わせ てユースケースを実現する Domain 層 - ビジネスロジックをコードに落とし込 む場所 Infrastructure 層 - @Repository アノテーション - DB・外部 API とのやりとり
  28. 「オニオンアーキテクチャ」と例外 • 各層の役割(Spring Boot による簡易版) 例外設計とモデリング 52 Domain 層 UseCase

    層 Infrastructure 層 Presentation 層 Presentation 層 - @RestController アノテーション - エンドポイントの定義 - リクエスト/レスポンス UseCase 層 - @Service アノテーション - ドメインオブジェクトを組み合わせ てユースケースを実現する Domain 層 - ビジネスロジックをコードに落とし込 む場所 Infrastructure 層 - @Repository アノテーション - DB・外部 API とのやりとり 例外はどこで書く︖
  29. • Domain のインタフェースを 実装するため Infra の独⾃ 定義はない(はず) • 予期する技術的例外 は

    Domain のインタ フェースから定義 • DB やネットワーク起因の予 期しない技術的例外はここで 発⽣する可能性がある 「オニオンアーキテクチャ」と例外の具体例 • Web API で例外についての⼀例 例外設計とモデリング 53 Domain 層 UseCase 層 Infrastructure 層 Presentation 層 • API 仕様に反するリクエストを バリデーションエラー • UseCase が throw した例外 をハンドリングしてレスポンス • ⼤域脱出した例外を catch し てレスポンス • Domain が throw した例外 からビジネスロジックを継続と中 断を判断して例外を throw • ドメインルールに違反するロジッ クを例外として throw • 予期する例外は try/catch、 予期しない例外は⼤域脱出
  30. • API 仕様に反するリクエストを バリデーションエラー • UseCase が throw した例外 をハンドリングしてレスポンス

    • ⼤域脱出した例外を catch し てレスポンス • Domain のインタフェースを 実装するため Infra の独⾃ 定義はない(はず) • 予期する技術的例外 は Domain のインタ フェースから定義 • DB やネットワーク起因の予 期しない技術的例外はここで 発⽣する可能性がある 「オニオンアーキテクチャ」と例外の具体例 • Web API で例外についての⼀例 例外設計とモデリング 54 Domain 層 UseCase 層 Infrastructure 層 Presentation 層 • Domain が throw した例外 からビジネスロジックを継続と中 断を判断して例外を throw • ドメインルールに違反するロジッ クを例外として throw • 予期する例外は try/catch、 予期しない例外は⼤域脱出 @RestController class XXXController(...) { @GetMapping(...) fun xxxMethod(...): ResponseEntity<...> { try { val xxx = xxxUseCase.execute(xxx) } catch(...) { throw XxxUseCaseErrorException(...) } return ResponseEntity(...) } data class XXXUseCaseErrorException(val error: XXXArticleUseCase.Error) : Exception() @ExceptionHandler(value = [XXXUseCaseErrorException::class]) fun onShowArticleUseCaseErrorException(e: XXXUseCaseErrorException): ResponseEntity<...> = { ... return ResponseEntity<...>( GenericErrorModel(...), ... ) } • UseCase が throw した例外 をハンドリング • API 仕様に反するリクエストをバリデー ションエラーとして throw Presentation 層
  31. • API 仕様に反するリクエストを バリデーションエラー • UseCase が throw した例外 をハンドリングしてレスポンス

    • ⼤域脱出した例外を catch し てレスポンス • Domain のインタフェースを 実装するため Infra の独⾃ 定義はない(はず) • 予期する技術的例外 は Domain のインタ フェースから定義 • DB やネットワーク起因の予 期しない技術的例外はここで 発⽣する可能性がある 「オニオンアーキテクチャ」と例外の具体例 • Web API で例外についての⼀例 例外設計とモデリング 55 Domain 層 UseCase 層 Infrastructure 層 Presentation 層 • Domain が throw した例外 からビジネスロジックを継続と中 断を判断して例外を throw • ドメインルールに違反するロジッ クを例外として throw • 予期する例外は try/catch、 予期しない例外は⼤域脱出 Presentation 層 @RestControllerAdvice class GlobalExceptionHandleController { @ExceptionHandler(NoResourceUnexpectedBusinessException: :class) fun noResourceFoundExceptionHandler(e: NoResourcenexpectedBusinessException): ResponseEntity<...> { return ResponseEntity<...>( GenericErrorModel( errors = GenericErrorModelErrors( body = listOf(”...") ), ), HttpStatus.NOT_FOUND ) } ... } • ⼤域脱出した例外を catch してレスポンス • 予期しない技術的・ビジネス例外をハンドリングする • この例だと、予期しないビジネス例外を 404 としてハンドリ ングした例
  32. • Domain のインタフェースを 実装するため Infra の独⾃ 定義はない(はず) • 予期する技術的例外 は

    Domain のインタ フェースから定義 • DB やネットワーク起因の予 期しない技術的例外はここで 発⽣する可能性がある 「オニオンアーキテクチャ」と例外の具体例 • Web API で例外についての⼀例 例外設計とモデリング 56 Domain 層 UseCase 層 Infrastructure 層 Presentation 層 • Domain が throw した例外 からビジネスロジックを継続と中 断を判断して例外を throw • ドメインルールに違反するロジッ クを例外として throw • 予期する例外は try/catch、 予期しない例外は⼤域脱出 UseCase 層 interface XxxUseCase { fun execute(...): Zzz = throw NotImplementedError() } @Service class XxxUseCaseImpl(...) : XxxUseCase { override fun execute(...): Zzz { try { val a = domain.DomainObject(...) } catch (...) { throw ExpectedDomainObjectException } ... return z } } • Domain が throw した例外からビジネスロジックの継続と中断 を判断 • 予期する例外だったら catch する • 予期しない例外だったら catch しない • API 仕様に反するリクエストを バリデーションエラー • UseCase が throw した例外 をハンドリングしてレスポンス • ⼤域脱出した例外を catch し てレスポンス
  33. • API 仕様に反するリクエストを バリデーションエラー • UseCase が throw した例外 をハンドリングしてレスポンス

    • ⼤域脱出した例外を catch し てレスポンス 「オニオンアーキテクチャ」と例外の具体例 • Web API で例外についての⼀例 例外設計とモデリング 57 Domain 層 UseCase 層 Infrastructure 層 Presentation 層 • Domain が throw した例外 からビジネスロジックを継続と中 断を判断して例外を throw • ドメインルールに違反するロジッ クを例外として throw • 予期する例外は try/catch、 予期しない例外は⼤域脱出 • Domain のインタフェースを 実装するため Infra の独⾃ 定義はない(はず) • DB やネットワーク起因の予 期しない技術的例外はここで 発⽣する可能性がある Domain 層 class DomainObject private constructor(...) { companion object { fun newWithValidation(val a String, val b Int): DomainObject { // 予期するビジネス例外 throw DomainExpectedException(...) return DomainObject() } fun newYYY(...) { // 予期しないビジネス例外 throw UnexpectedException(...) return DomainObject() } } } • ドメインルールに違反するロジックを例外として throw • 予期する例外は呼び出し側が try/catch、予期しない例外 は⼤域脱出してフレームワークがハンドリング • ドメイン側がクライアント側をどうするかは検討不要
  34. • API 仕様に反するリクエストを バリデーションエラー • UseCase が throw した例外 をハンドリングしてレスポンス

    • ⼤域脱出した例外を catch し てレスポンス 「オニオンアーキテクチャ」と例外の具体例 • Web API で例外についての⼀例 例外設計とモデリング 58 Domain 層 UseCase 層 Infrastructure 層 Presentation 層 • Domain が throw した例外 からビジネスロジックを継続と中 断を判断して例外を throw • ドメインルールに違反するロジッ クを例外として throw • 予期する例外は try/catch、 予期しない例外は⼤域脱出 • Domain のインタフェースを 実装するため Infra の独⾃ 定義はない(はず) • 予期する技術的例外 は Domain のインタ フェースから定義 • DB やネットワーク起因の予 期しない技術的例外はここで 発⽣する可能性がある Infrastructure 層 @Repository class XxxRepositoryImpl(...) : XxxRepository { override fun findBySlug(slug: Slug): YYY { val x = Hoge() // 予期する技術的例外 ... throw TechnicalExpectedError(...) return yyy } }
  35. モニタリング(監視) • 意味 • あるシステムやそのシステムのコンポーネントの振る舞いや出⼒を観察しチェックし続けること • 具体例 • メモリ・CPU 使⽤率

    モニタリング・オブザーバビリティと例外 62 > 監視とは、あるシステムやそのシステムのコンポーネントの振る舞いや出⼒を観察しチェックし > 続ける⾏為である。 「⼊⾨ 監視」 p.35 より引⽤
  36. オブザーバビリティ • 意味 • 「システムがどのような状態になったとしても、それがどんなに斬新で奇妙なものであっても、ど れだけ理解し説明できるかを⽰す尺度」 • o11y(Observability) と略されることも モニタリング・オブザーバビリティと例外

    63 > 簡単に⾔うと、私たちが考えるソフトウェアシステムの「オブザーバビリティ」とは、 システムがどのよう > な状態になったとしても、それがどんなに斬新で奇妙なものであっても、どれだけ理解し説明できる > かを⽰す尺度です。 また、そのような斬新で奇妙な状態に対しても、事前にデバッグの必要性を > 定義したり予測したりすることなく、 システムの状態データのあらゆるディメンションやそれらの組み > 合わせについてアドホックに調査し、よりデバッグが可能であるようにする必要があります。 もし、 新 > しいコードをデプロイする必要がなく 、どんな斬新で奇妙な状態でも理解できるなら、オブザーバビ > リティがあると⾔えます。 「オブザーバビリティ・エンジニアリング」 p.6 より引⽤
  37. SLOの値が意味する可⽤性表 • SLO を具体的な時間に置き換えると下表のようになる モニタリング・オブザーバビリティと例外 65 可⽤性(%) 年あたりダウンタイム ⽉あたりダウンタイム 週あたりダウンタイム

    ⽇あたりダウンタイム 90%(ワンナイン) 36.5⽇ 72時間 16.8時間 2.4時間 95% 18.25⽇ 36時間 8.4時間 1.2時間 99%(ツーナイン) 3.65⽇ 7.20時間 1.68時間 14.4分 99.5% 1.83⽇ 3.60時間 50.4分 7.2分 99.9%(スリーナイン) 8.76時間 43.8分 10.1分 1.44分 99.95% 4.38時間 21.56分 5.04分 43.2秒 99.99%(フォーナイン) 52.56分 4.38分 1.01分 8.64秒 99.995% 26.28分 2.16分 30.24秒 4.32秒 99.999%(ファイブナイン) 5.26分 25.9秒 6.05秒 864.3ミリ秒 可⽤性表
  38. SLO とオンコール対応 • SLO 99.9 % でオンコール対応について考える モニタリング・オブザーバビリティと例外 66 可⽤性(%)

    年あたりダウンタイム ⽉あたりダウンタイム 週あたりダウンタイム ⽇あたりダウンタイム 90%(ワンナイン) 36.5⽇ 72時間 16.8時間 2.4時間 95% 18.25⽇ 36時間 8.4時間 1.2時間 99%(ツーナイン) 3.65⽇ 7.20時間 1.68時間 14.4分 99.5% 1.83⽇ 3.60時間 50.4分 7.2分 99.9%(スリーナイン) 8.76時間 43.8分 10.1分 1.44分 99.95% 4.38時間 21.56分 5.04分 43.2秒 99.99%(フォーナイン) 52.56分 4.38分 1.01分 8.64秒 99.995% 26.28分 2.16分 30.24秒 4.32秒 99.999%(ファイブナイン) 5.26分 25.9秒 6.05秒 864.3ミリ秒 可⽤性表 参考: https://speakerdeck.com/ymotongpoo/reliability-objective-and-system-architecture
  39. SLO とオンコール対応 • オンコール対応について考えてみる モニタリング・オブザーバビリティと例外 67 可⽤性(%) 年あたりダウンタイム ⽉あたりダウンタイム 週あたりダウンタイム

    ⽇あたりダウンタイム 90%(ワンナイン) 36.5⽇ 72時間 16.8時間 2.4時間 95% 18.25⽇ 36時間 8.4時間 1.2時間 99%(ツーナイン) 3.65⽇ 7.20時間 1.68時間 14.4分 99.5% 1.83⽇ 3.60時間 50.4分 7.2分 99.9%(スリーナイン) 8.76時間 43.8分 10.1分 1.44分 99.95% 4.38時間 21.56分 5.04分 43.2秒 99.99%(フォーナイン) 52.56分 4.38分 1.01分 8.64秒 99.995% 26.28分 2.16分 30.24秒 4.32秒 99.999%(ファイブナイン) 5.26分 25.9秒 6.05秒 864.3ミリ秒 可⽤性表 1 ヶ⽉の SLO を「99.9 %」としていたとき、ダウンタイム「43.8 分」で以下の対応を終えられれば OK 0. 問題発⽣ 1. アラート発報 2a. 担当者(アラート輪番が)アラートに反応 2b. 担当者(アラート輪番が)が担当チームにエスカレーション 3. 調査開始 4. 原因特定 5a. 必要/可能ならロールバック 5b. 問題箇所を修正 6a. アプリケーションならアーティファクトをビルド 6b. ミドルウェア等なら設定ファイルを検証 7a. デプロイ承認依頼 7b. 許可後、デプロイ依頼 7c. 修正をデプロイ 8. 問題が解消されたことを確認 参考: https://speakerdeck.com/ymotongpoo/reliability-objective-and-system-architecture
  40. SLO とオンコール対応 • SLO を守るためにできること モニタリング・オブザーバビリティと例外 68 可⽤性(%) 年あたりダウンタイム ⽉あたりダウンタイム

    週あたりダウンタイム ⽇あたりダウンタイム 90%(ワンナイン) 36.5⽇ 72時間 16.8時間 2.4時間 95% 18.25⽇ 36時間 8.4時間 1.2時間 99%(ツーナイン) 3.65⽇ 7.20時間 1.68時間 14.4分 99.5% 1.83⽇ 3.60時間 50.4分 7.2分 99.9%(スリーナイン) 8.76時間 43.8分 10.1分 1.44分 99.95% 4.38時間 21.56分 5.04分 43.2秒 99.99%(フォーナイン) 52.56分 4.38分 1.01分 8.64秒 99.995% 26.28分 2.16分 30.24秒 4.32秒 99.999%(ファイブナイン) 5.26分 25.9秒 6.05秒 864.3ミリ秒 可⽤性表 1 ヶ⽉の SLO を「99.9 %」としていたとき、ダウンタイム「43.8 分」で以下の対応を終えられれば OK 0. 問題発⽣ 1. アラート発報 2a. 担当者(アラート輪番が)アラートに反応 2b. 担当者(アラート輪番が)が担当チームにエスカレーション 3. 調査開始 4. 原因特定 5a. 必要/可能ならロールバック 5b. 問題箇所を修正 6a. アプリケーションならアーティファクトをビルド 6b. ミドルウェア等なら設定ファイルを検証 7a. デプロイ承認依頼 7b. 許可後、デプロイ依頼 7c. 修正をデプロイ 8. 問題が解消されたことを確認 SLO を守るためにできること • 「完全に落ちること」だと定義するなら、冗⻑化する • 99.9 % の信頼性を冗⻑化して 1-(1-0.999)*(1-0.999)=99.99999 とか • ⾃動化できる部分を増やす • リリース後、バーンレートが⾼かったら、すぐにロールバックできる • 承認スピードを向上させる • 承認へのラインを増やす • SLI に誤りがあれば SLI を⾒直す • SLI がビジネスの振る舞いを反映できているのか考える • オブザーバビリティを向上させる施策をうつ • 計装の場所を増やす • ユースケースを⾒直す • (特に)技術的例外とビジネス例外を混合していないかなど 参考: https://speakerdeck.com/ymotongpoo/reliability-objective-and-system-architecture
  41. SLO とオンコール対応 • SLO を守るためにできること モニタリング・オブザーバビリティと例外 69 可⽤性(%) 年あたりダウンタイム ⽉あたりダウンタイム

    週あたりダウンタイム ⽇あたりダウンタイム 90%(ワンナイン) 36.5⽇ 72時間 16.8時間 2.4時間 95% 18.25⽇ 36時間 8.4時間 1.2時間 99%(ツーナイン) 3.65⽇ 7.20時間 1.68時間 14.4分 99.5% 1.83⽇ 3.60時間 50.4分 7.2分 99.9%(スリーナイン) 8.76時間 43.8分 10.1分 1.44分 99.95% 4.38時間 21.56分 5.04分 43.2秒 99.99%(フォーナイン) 52.56分 4.38分 1.01分 8.64秒 99.995% 26.28分 2.16分 30.24秒 4.32秒 99.999%(ファイブナイン) 5.26分 25.9秒 6.05秒 864.3ミリ秒 可⽤性表 1 ヶ⽉の SLO を「99.9 %」としていたとき、ダウンタイム「43.8 分」で以下の対応を終えられれば OK 0. 問題発⽣ 1. アラート発報 2a. 担当者(アラート輪番が)アラートに反応 2b. 担当者(アラート輪番が)が担当チームにエスカレーション 3. 調査開始 4. 原因特定 5a. 必要/可能ならロールバック 5b. 問題箇所を修正 6a. アプリケーションならアーティファクトをビルド 6b. ミドルウェア等なら設定ファイルを検証 7a. デプロイ承認依頼 7b. 許可後、デプロイ依頼 7c. 修正をデプロイ 8. 問題が解消されたことを確認 SLO を守るためにできること • 「完全に落ちること」だと定義するなら、冗⻑化する • 99.9 % の信頼性を冗⻑化して 1-(1-0.999)*(1-0.999)=99.99999 とか • ⾃動化できる部分を増やす • リリース後、バーンレートが⾼かったら、すぐにロールバックできる • 承認スピードを向上させる • 承認へのラインを増やす • SLI に誤りがあれば SLI を⾒直す • SLI がビジネスの振る舞いを反映できているのか考える • オブザーバビリティを向上させる施策をうつ • 計装の場所を増やす • ユースケースを⾒直す • (特に)技術的例外とビジネス例外を混合していないかなど 本発表では、例外設計から SLO を守るためにできることを考える 参考: https://speakerdeck.com/ymotongpoo/reliability-objective-and-system-architecture
  42. 企画・開発・運⽤の⼀連 モニタリング・オブザーバビリティと例外設計から考えるログ設計 70 企画 ・ ユースケースの定義 ・ SLO の合意 開発

    ・ 例外を実装 ・ エラーログ 運⽤ ・ アラート設計 監視 ・ SLO ・ バーンレート アラート 評価 正しいユースケース理解と例外設計をしなければならない ・ログやステータスコードが適切でなければ、SLO は誤った値になる ・誤った SLO によるオンコールはアラート疲れを引き起こし、無視するようになる
  43. 企画・開発・運⽤の⼀連 モニタリング・オブザーバビリティと例外設計から考えるログ設計 71 企画 ・ ユースケースの定義 ・ SLO の合意 開発

    ・ 例外を実装 ・ エラーログ 運⽤ ・ アラート設計 監視 ・ SLO ・ バーンレート アラート 評価 正しいユースケース理解と例外設計をしなければならない ・ログやステータスコードが適切でなければ、SLO は誤った値になる ・誤った SLO によるオンコールはアラート疲れを引き起こし、無視するようになる 今回は、例外設計の話なので、エラーログに考える
  44. 「オニオンアーキテクチャ」と例外の振り返り(各層の例外) • Web API で各層と例外についての⼀例 モニタリング・オブザーバビリティと例外設計から考えるログ設計 72 Domain 層 UseCase

    層 Infrastructure 層 Presentation 層 Presentation 層 - @RestController アノテーション - エンドポイントの定義 - リクエスト/レスポンス UseCase 層 - @Service アノテーション - ドメインオブジェクトを組み合わせ てユースケースを実現する Domain 層 - ビジネスロジックをコードに落とし込 む場所 Infrastructure 層 - @Repository アノテーション - DB・外部 API とのやりとり
  45. 「オニオンアーキテクチャ」と例外の振り返り(各層の例外) • Web API で各層と例外についての⼀例 モニタリング・オブザーバビリティと例外設計から考えるログ設計 73 Domain 層 UseCase

    層 Infrastructure 層 Presentation 層 Presentation 層 - @RestController アノテーション - エンドポイントの定義 - リクエスト/レスポンス UseCase 層 - @Service アノテーション - ドメインオブジェクトを組み合わせ てユースケースを実現する Domain 層 - ビジネスロジックをコードに落とし込 む場所 Infrastructure 層 - @Repository アノテーション - DB・外部 API とのやりとり ログ出⼒はどこに書く︖
  46. 「オニオンアーキテクチャ」と例外の振り返り(各層の例外) • Web API で各層と例外についての⼀例 モニタリング・オブザーバビリティと例外設計から考えるログ設計 74 Domain 層 UseCase

    層 Infrastructure 層 Presentation 層 Presentation 層 - @RestController アノテーション - エンドポイントの定義 - リクエスト/レスポンス UseCase 層 - @Service アノテーション - ドメインオブジェクトを組み合わせ てユースケースを実現する Domain 層 - ビジネスロジックをコードに落とし込 む場所 Infrastructure 層 - @Repository アノテーション - DB・外部 API とのやりとり Domain 層か UseCase 層が候補
  47. 例外のログを書く場所について考える モニタリング・オブザーバビリティと例外設計から考えるログ設計 75 候補 理由 UseCase 層 • 例外の原因の推測が容易 •

    ログを⾒ればコードを特定できる状態 • ユースケースの仕様と重要度の⼀致が容易 • info/warn/error/critical を記述しやすい Domain 層 • ドメイン知識から throw する例外が明確になる • 予期しない例外なら Error/Critical なエラーを出す • 予期しないドメインの例外のログを書く場所が他にない • @RestControllerAdvice にログを出すこともできるが、技術的例外 とビジネス例外のログが混合する可能性がある 表︓候補と理由
  48. 「オニオンアーキテクチャ」と例外の振り返り(各層の例外) • Web API で各層と例外についての⼀例 モニタリング・オブザーバビリティと例外設計から考えるログ設計 76 Domain 層 UseCase

    層 Infrastructure 層 Presentation 層 Presentation 層 - @RestController アノテーション - エンドポイントの定義 - リクエスト/レスポンス UseCase 層 - @Service アノテーション - ドメインオブジェクトを組み合わせ てユースケースを実現する Domain 層 - ビジネスロジックをコードに落とし込 む場所 Infrastructure 層 - @Repository アノテーション - DB・外部 API とのやりとり Domain 層か UseCase 層が候補 interface XxxUseCase { fun execute(...): Zzz = throw NotImplementedError() } @Service class XxxUseCaseImpl(...) : XxxUseCase { override fun execute(...): Zzz { try { val a = domain.DomainObject(...) } catch (...) { // 記述しない場合もある log.stdout(”info", "xxxがyyyでした") throw ExpectedDomainObjectException } ... return z } }
  49. 「オニオンアーキテクチャ」と例外の振り返り(各層の例外) • Web API で各層と例外についての⼀例 モニタリング・オブザーバビリティと例外設計から考えるログ設計 77 Domain 層 UseCase

    層 Infrastructure 層 Presentation 層 Presentation 層 - @RestController アノテーション - エンドポイントの定義 - リクエスト/レスポンス UseCase 層 - @Service アノテーション - ドメインオブジェクトを組み合わせ てユースケースを実現する Domain 層 - ビジネスロジックをコードに落とし込 む場所 Infrastructure 層 - @Repository アノテーション - DB・外部 API とのやりとり Domain 層か UseCase 層が候補 class DomainObject private constructor(...) { companion object { fun newWithValidation(val a String, val b Int): DomainObject { // 予期するビジネス例外(記述しない場合もある) log.stdout(“info”, “vvvでbbbが発⽣しました”) throw DomainExpectedException(...) return DomainObject() } fun newYYY(...) { // 予期しないビジネス例外(記述する) log.stderr(“critical”, “zzzでxxxが発⽣しました”) throw UnexpectedException(...) return DomainObject() } } }
  50. 例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践 • 「例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践」⽬的 • 例外設計を Arrow-kt

    の Either 型でより実践的に • Spring Boot まで活⽤したときの説明 80 企画 ・ 要件定義 ・ ユースケースの定義 ・ SLO の合意 開発 ・ 例外を実装 ・ ログ 運⽤ ・ アラート設計 ・ SLO ・ バーンアラート アラート 評価 「例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践」の範囲
  51. 例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践 • Arrow-kt について • Kotlin に関数型のパラダイムを持ち込むライブラリ •

    「Arrow brings idiomatic functional programming to Kotlin」 • さまざまな機能があるが、今回の発表では Either 型だけ利⽤ 81 出典: https://arrow-kt.io/
  52. 例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践 • Arrow-kt の Either 型を⽤いたときの変化(1/3) • Kotlin

    の Exception について • Kotlin には検査例外がないので、コンパイルエラーでハンドリング忘れを防⽌できない • try/catch の判断を実装の内部までみる必要がある(予期する・しない例外も同様) 82 fun divide(a: Int, b: Int): Int { if (b == 0) { throw IllegalArgumentException("除数を 0 にしてはい けません") } return a / b } try { val result = divide(a, b) println(result) } catch (e: Exception) { println("${e.message}") } Exception による try catch Exception がメソッドシグネチャに含まれていない -> 呼び出し側は、Exception の発⽣を予測できない 呼び出し側が実装の詳細を知る必要がある -> try/catch を忘れる可能性がある
  53. • Arrow-kt の Either 型を⽤いたときの変化(2/3) • Arrow-kt の Either 型

    • 成功/失敗ケースを型で表現できる • Either 型の評価をしなければ値を利⽤できない 例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践 83 Either 型による値の評価(⼀例) メソッドシグネチャに Either 型が含まれている -> 呼び出し側が成功/失敗ケースを把握できる 呼び出し側は、Either の評価をしなければ値を利⽤ できない(コンパイルエラーが発⽣する) -> ハンドリングを忘れない fun divide(a: Int, b: Int): Either<String, Int> { return when (b == 0) { true -> “除数を 0 にしてはいけません”.left() false -> (a / b).right() } } val result = divide(a, b) when (result) { is Either.Right -> { println(“除算結果 ${result.value}") } is Either.Left -> { println(result.value) } }
  54. • Arrow-kt の Either 型を⽤いたときの変化(3/3) • Sealed Interface と Either

    型を組み合わせた⾃作エラー型 • Left 型を Sealed Interface で記述すると失敗パターンの⾃作エラー型を作れる • 複数の失敗ケースのハンドリングを簡潔に記述しやすくなる 例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践 84 Either 型による値の評価 成功パターンの型 戻り値の Either.Left を Sealed Interface にする Either<ApiCallError, ApiCallSuccess> data class ApiCallSuccess(val message: String) sealed interface ApiCallError { data class NotFound(val message: String) : ApiCallError object ServerError : ApiCallError } fun apiCall(): Either<ApiCallError, ApiCallSuccess> { return when (...) { // 条件式 A -> ApiCallSuccess(...).right() B -> ApiCallError.NotFound(...).left() C -> ApiCallError.ServerError.left() } } val result = apiCall() when (result) { is Either.Left -> when (val it = result.value) { is ApiCallError.NotFound -> ... is ApiCallError.ServerError -> ... } is Either.Right -> { ... } } 失敗パターンの⾃作エラー型(Sealed Interface) Either を評価(Right or Left)した後、 失敗ケースをさらに評価する
  55. Arrow-kt を例外の分類へ適⽤(1/2) • 「技術的例外・ビジネス例外」と「予期する例外・予期しない例外」(再掲) • 前節(Arrow-kt の紹介前)までは、どのように例外を catch するかまでだった •

    予期する/予期しない例外を catch するかは、実装の詳細を知る必要があった 例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践 85 技術的例外 ビジネス例外 予期する例外 予期しない例外 予期する 技術的例外 予期しない 技術的例外 予期しない ビジネス例外 予期する ビジネス例外
  56. Arrow-kt を例外の分類へ適⽤(2/2) • Arrow-kt の Either 型を利⽤した分類 • 予期する ->

    Either 型(⾃作エラー型)、予期しない -> Exception に分類する • ハンドリングの必要性を Either 型から判断、Exception は全て⼤域脱出 例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践 86 技術的例外 ビジネス例外 予期する例外 予期しない例外 予期する 技術的例外 予期しない 技術的例外 予期しない ビジネス例外 予期する ビジネス例外 予期する -> Either 型 予期しない -> Exception
  57. Kotlin Conf 2024 にて • 「Unlocking the Power of Arrow

    2.0: A Comprehensive Guide」 • Arrow のメンテナによる発表 • 主に Arrow 2.0 についての発表で、Either と Exception についても⾔及 例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践 87 Kotlin Conf 2024 Simon Vergauwen (出典: https://kotlinconf.com/speakers/ea32e9a2-b68e-44c5-b4b7-6d2aa0dc3141/ )
  58. 「Unlocking the Power of Arrow 2.0: A Comprehensive Guide」について •

    発表において • “Exception” と “Typed Errors” と呼ばれていた • 例外の分類 • “Exception” • “Fatal exception”, “Unexpected exception, non-business related” • 本発表における「予期しない技術的例外」 • “Typed Error” • “Prevent forgetting about business errors” • 本発表における「予期するビジネス例外」、⾃作エラー型 例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践 88
  59. Spring Boot × Arrow-kt(Either 型) • Web API で組み込んだ時の⼀例 例外設計を

    Kotlin(× Arrow-kt)の⾃作エラー型で実践 89 Domain 層 UseCase 層 Infrastructure 層 Presentation 層 • UseCase の戻り値である Either 型を評価してレスポンス • 予期する例外(⾃作エ ラー型)をハンドリング • ⼤域脱出した例外を catch し てレスポンス • 予期しない例外をハンド リングする • Domain の戻り値の Either 型を評価して処理の継続と中 断を評価 • Either.Right -> 継続 • Either.Left -> 中断 • ドメインルールから、予期する例 外を判断 • 予期する例外 -> Either 型(⾃作エラー 型)を return • 予期しない例外 -> Exception を throw • Domain のインタフェースを 実装する • 予期する例外は Domain の Sealed インタフェースから定義 • 予期しない例外は Exception を throw
  60. Spring Boot × Arrow-kt(Either 型) • Web API で組み込んだ時の⼀例 例外設計を

    Kotlin(× Arrow-kt)の⾃作エラー型で実践 90 Domain 層 UseCase 層 Infrastructure 層 Presentation 層 • UseCase の戻り値である Either 型を評価してレスポンス • 予期する例外(⾃作エ ラー型)をハンドリング • ⼤域脱出した例外を catch し てレスポンス • 予期しない例外をハンド リング • Domain の戻り値の Either 型を評価して処理の継続と中 断を評価 • Either.Right -> 継続 • Either.Left -> 中断 • ドメインルールから、予期する例 外を判断 • 予期する例外 -> Either 型(⾃作エラー 型)を return • 予期しない例外 -> Exception を throw • Domain のインタフェースを 実装する • 予期する例外は Domain の Sealed インタフェースから定義 • 予期しない例外は Exception を throw @RestController class XXXController(...) { @GetMapping(...) fun getXxx(...): ResponseEntity<xxx> { val xxx = xxxUseCase.execute(xxx).getOrElse { throw XXXUseCaseErrorException(it) } return ResponseEntity(...) } data class XxxUseCaseErrorException(val error: XXXArticleUseCase.Error) : Exception() @ExceptionHandler(value = [XxxUseCaseErrorException::class]) fun onXxxUseCaseErrorException(e: XxxUseCaseErrorException): ResponseEntity<...> = // ⾃作エラー型の Sealed Interface で分岐 when (val error = e.error) { is yyy -> ResponseEntity<...>( GenericErrorModel(...), HttpStatus.NOT_FOUND ) is zzz -> ResponseEntity<...>( GenericErrorModel(...), HttpStatus.BAD_REQUEST ) } } • UseCase の戻り値が Either.Left のとき Exception ハンドリング • UseCase の戻り値が • Either.Right だったとき -> 正常系の Response • Either.Left だったときに -> 異常系の Response • UseCase の Either.Left の Sealed Interface に合わせて 分岐 Presentation 層
  61. Spring Boot × Arrow-kt(Either 型) • Web API で組み込んだ時の⼀例 例外設計を

    Kotlin(× Arrow-kt)の⾃作エラー型で実践 91 Domain 層 UseCase 層 Infrastructure 層 Presentation 層 • UseCase の戻り値である Either 型を評価してレスポンス • 予期する例外(⾃作エ ラー型)をハンドリング • ⼤域脱出した例外を catch し てレスポンス • 予期しない例外をハンド リング • Domain の戻り値の Either 型を評価して処理の継続と中 断を評価 • Either.Right -> 継続 • Either.Left -> 中断 • ドメインルールから、予期する例 外を判断 • 予期する例外 -> Either 型(⾃作エラー 型)を return • 予期しない例外 -> Exception を throw • Domain のインタフェースを 実装する • 予期する例外は Domain の Sealed インタフェースから定義 • 予期しない例外は Exception を throw @RestControllerAdvice class GlobalExceptionHandleController { @ExceptionHandler(NoResourceUnexpectedBusinessException: :class) fun noResourceFoundExceptionHandler(e: NoResourcenexpectedBusinessException): ResponseEntity<...> { return ResponseEntity<...>( GenericErrorModel( errors = GenericErrorModelErrors( body = listOf(”...") ), ), HttpStatus.NOT_FOUND ) } ... } • ⼤域脱出した例外を catch してレスポンス(修正前と同じ) • 予期しない技術的・ビジネス例外をハンドリングする • Exception はすべてここでハンドリングされるのが明確になる Presentation 層
  62. Spring Boot × Arrow-kt(Either 型) • Web API で組み込んだ時の⼀例 例外設計を

    Kotlin(× Arrow-kt)の⾃作エラー型で実践 92 Domain 層 UseCase 層 Infrastructure 層 Presentation 層 • UseCase の戻り値である Either 型を評価してレスポンス • 予期する例外(⾃作エ ラー型)をハンドリング • ⼤域脱出した例外を catch し てレスポンス • 予期しない例外をハンド リング • Domain の戻り値の Either 型を評価して処理の継続と中 断を評価 • Either.Right -> 継続 • Either.Left -> 中断 • ドメインルールから、予期する例 外を判断 • 予期する例外 -> Either 型(⾃作エラー 型)を return • 予期しない例外 -> Exception を throw • Domain のインタフェースを 実装する • 予期する例外は Domain の Sealed インタフェースから定義 • 予期しない例外は Exception を throw UseCase 層 interface XxxUseCase { fun execute(...): Either<Error, DomainObject> = throw NotImplementedError() // ⾃作エラー型 sealed interface Error { data class Yyy(...) : Error data class Zzz(...) : Error } } @Service class XxxUseCaseImpl(...) : XxxUseCase { override fun execute(...): Either<XxxUseCase.Error, DomainObject> { // 失敗ケースの場合は、UseCase の⾃作エラー型に詰め替える val aaa = DomainObject.new(...).getOrElse { return XxxUseCase.Error.Yyy(...).left() } // ... return domainObejct.right() } } • UseCase の⾃作エラー型 • ユースケースの失敗ケース に合わせて作成 • ドメインオブジェクトで予期する例 外(⾃作エラー型)が発⽣した 場合、UseCase の⾃作エラー 型に詰め替え • 成功した場合は Either.Right を return
  63. Spring Boot × Arrow-kt(Either 型) • Web API で組み込んだ時の⼀例 例外設計を

    Kotlin(× Arrow-kt)の⾃作エラー型で実践 93 Domain 層 UseCase 層 Infrastructure 層 Presentation 層 • UseCase の戻り値である Either 型を評価してレスポンス • 予期する例外(⾃作エ ラー型)をハンドリング • ⼤域脱出した例外を catch し てレスポンス • 予期しない例外をハンド リング • Domain の戻り値の Either 型を評価して処理の継続と中 断を評価 • Either.Right -> 継続 • Either.Left -> 中断 • ドメインルールから、予期する例 外を判断 • 予期する例外 -> Either 型(⾃作エラー 型)を return • 予期しない例外 -> Exception を throw • Domain のインタフェースを 実装する • 予期する例外は Domain の Sealed インタフェースから定義 • 予期しない例外は Exception を throw Domain 層 class DomainObject private constructor(...) { companion object { fun newWithValidation(val a String, val b Int): Either<DomainExpectedException, DomainObject> { // 予期するビジネス例外は⾃作エラーで詰め替え return XxxError.Yyy(...).left() // ... return DomainObject().right() } fun newYYY(...): Either<...>{ // 予期しないビジネス例外は throw throw UnexpectedException(...) // ... return DomainObject().right } } } • 予期する例外の場合は、 Domain の⾃作エラー型で詰 め替え • このコード例では Sealed Interface を省略 • 予期しない例外の場合は、 Exception を throw
  64. Spring Boot × Arrow-kt(Either 型) • Web API で組み込んだ時の⼀例 例外設計を

    Kotlin(× Arrow-kt)の⾃作エラー型で実践 94 Domain 層 UseCase 層 Infrastructure 層 Presentation 層 • UseCase の戻り値である Either 型を評価してレスポンス • 予期する例外(⾃作エ ラー型)をハンドリング • ⼤域脱出した例外を catch し てレスポンス • 予期しない例外をハンド リング • Domain の戻り値の Either 型を評価して処理の継続と中 断を評価 • Either.Right -> 継続 • Either.Left -> 中断 • ドメインルールから、予期する例 外を判断 • 予期する例外 -> Either 型(⾃作エラー 型)を return • 予期しない例外 -> Exception を throw • Domain のインタフェースを 実装する • 予期する例外は Domain の Sealed インタフェースから定義 • 予期しない例外は Exception を throw Infrastructure 層 @Repository class XxxRepositoryImpl(...): XxxRepository { override fun findBySlug(slug: Slug): Either<Zzz, Yyy> { val x = Hoge() ... // 予期する例外(⾃作エラー型) return XxxRepository.Error.Nnn(...).left() return yyy.right } }
  65. 例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践 • 「例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践」まとめ • 例外設計を Arrow-kt

    の Either 型でより実践的になることを説明 • 予期する/予期しないが Either と Exception で役割を分けた • Spring Boot まで活⽤したとき、簡潔な実装にできる 95 企画 ・ 要件定義 ・ ユースケースの定義 ・ SLO の合意 開発 ・ 例外を実装 ・ ログ 運⽤ ・ アラート設計 ・ SLO ・ バーンアラート アラート 評価 「例外設計を Kotlin(× Arrow-kt)の⾃作エラー型で実践」の範囲
  66. まとめ 「例外設計について考えて Kotlin で実践する」のまとめ • 例外設計を考えることで、プロダクト開発へ好影響を考えた • 例外設計について、Kotlin で実践した 97

    企画 ・ 要件定義 ・ ユースケースの定義 ・ SLO の合意 開発 ・ 例外を実装 ・ ログ 運⽤ ・ アラート設計 ・ SLO ・ バーンアラート ・アラート ・評価 フィードバック
  67. まとめ 「例外設計について考えて Kotlin で実践する」のまとめ • 例外設計を考える背景 • 例外設計は、⼀連のプロダクト開発の影響を与える • 技術的例外・ビジネス例外と予期する例外・予期しない例外

    • 例外を分類する⽬的を把握し、例外を 4 種類に分類 • 例外設計とモデリング • DDD の⽂化から、例外を把握し、コードに落とし込む • 監視・オブザーバビリティと例外 • SLO 観点からログについて考えた • Kotlin(SpringBoot×Arrow-kt)で実践 • 例外の種別を Arrow-kt で表現し実装を拡張させた 98
  68. まとめ 宣伝 • 内容が重厚になったため、説明不⾜になった部分が多くあります • コードも断⽚的で、⽂化的な部分はスライドでは表現しきれませんでした • 「ハンズオンで学ぶサーバーサイド Kotlin」は、発表内容を超初歩的に実践し ています

    • 発表をもとに、例外設計に興味を持ち、Kotlin(Spring Boot × Arrow-kt)で 実践したい⽅は是⾮読んでみてください。Like をいただけると嬉しいです🙏 99 「ハンズオンで学ぶサーバーサイド Kotlin」
  69. 参考⽂献 書籍(1/2) • 和⽥ 卓⼈ (監修), Kevlin Henney (編集), 夏⽬

    ⼤ (翻訳)、『プログラマが知るべき97のこ と』, オライリージャパン, 2010/12/18, 276ページ • バートランド メイヤー (著), 酒匂 寛 (翻訳), 『オブジェクト指向⼊⾨ 第2版: 原則・コンセプト』, 翔泳社, 2007/1/1, 904ページ • Andrew Hunt (著), David Thomas (著), 村上雅章 (翻訳), 『達⼈プログラマー(第2版): 熟達に向けたあなたの旅』, オーム社, 2020/11/21, 422ページ • Dan Bergh Johnsson (著), Daniel Deogun (著), Daniel Sawano (著), 須⽥智之 (翻訳), 『セキュア・バイ・デザイン 安全なソフトウェア設計』, マイナビ出版, 2021/9/24, 560 ページ • Tom Long (著), 秋勇紀 (翻訳), ⾼⽥新⼭ (翻訳), ⼭本⼤祐 (監修) , 『 Good Code, Bad Code 〜持続可能な開発のためのソフトウェアエンジニア的思考』, 秀和システム, 2023/1/28, 432ページ • 松岡幸⼀郎(著), 『ドメイン駆動設計 モデリング/実装ガイド』 • 松岡幸⼀郎(著), 『ドメイン駆動設計 サンプルコード&FAQ』 100
  70. 参考⽂献 書籍(2/2) • ヴォーン・ヴァーノン (著), 髙⽊ 正弘 (翻訳), 『実践ドメイン駆動設』, 翔泳社,

    2015/3/16, 616ページ • エリック エヴァンス (著), 和智 右桂 (翻訳), 牧野 祐⼦ (翻訳), 『エリック・エヴァンスのドメイン駆動設計: ソフトウェアの核⼼にある複雑さに⽴ち向かう』,翔泳社, 2011/4/1, 538ページ • Vladimir Khorikov (著), 須⽥智之 (翻訳), 『単体テストの考え⽅/使い⽅』, 2022/12/28, 416 ページ • Scott Wlaschin, 『Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#』, Pragmatic Bookshelf, 2018/2/13, 312ページ • Mike Julian (著), 松浦 隼⼈ (翻訳), 『⼊⾨ 監視 ―モダンなモニタリングのためのデザインパター ン』,Pragmatic Bookshelf , オライリー・ジャパン, 2019/1/17, 228ページ • Charity Majors (著), Liz Fong-Jones (著), George Miranda (著), ⼤⾕ 和紀 (翻訳), ⼭⼝ 能迪 (翻訳), 『オブザーバビリティ・エンジニアリング』, オライリー・ジャパン, 2023/1/27, 336ページ • Alex Hidalgo (著), ⼭⼝ 能迪 (監修, 翻訳), 成⽥ 昇司 (翻訳), 『SLO サービスレベル⽬標 ―SLI、 SLO、エラーバジェット導⼊の実践ガイド』, オライリー・ジャパン, 2023/7/11, 432ページ 101
  71. 参考⽂献 ウェブサイト • scrapbox, 例外設計, https://scrapbox.io/kawasima/%E4%BE%8B%E5%A4%96%E8%A8%AD%E8%A8% 88, 6⽉14⽇(⾦) • Zenn,

    ハンズオンで学ぶサーバーサイド Kotlin(Spring Boot&Arrow)v2.0.1, https://zenn.dev/msksgm/books/implementing-server-side-kotlin-development, 6⽉ 14⽇(⾦) • NDC { Security }, Dan Bergh Johnsson, https://ndc-security.com/speakers/dan- bergh-johnsson, 6⽉14⽇(⾦) • Kotlin Conf 2024, SIMON VERGAUWEN, https://kotlinconf.com/speakers/ea32e9a2- b68e-44c5-b4b7-6d2aa0dc3141/, 6⽉14⽇(⾦) • Speaker Deck,信頼性⽬標とシステムアーキテクチャー / Reliability Objective and System Architecture, https://speakerdeck.com/ymotongpoo/reliability-objective-and-system- architecture 6⽉14⽇(⾦) • Arrow, Arrow, https://arrow-kt.io/, 6⽉16⽇(⽇) • Kotlin Conf 2024, Unlocking the Power of Arrow 2.0: A Comprehensive Guide, https://kotlinconf.com/talks/586193/, 6⽉19⽇(⽔) 102