モデルベースで要件定義をやってみた に行ってきた #現場で役立つモデル駆動設計

モデルベースで要件定義をやってみた に参加しました。簡単に所感をまとめます。

modeling-how-to-learn.connpass.com

所感

途中で言ってたけど、ステークホルダが RDRA のモデルでコミュニケーションできるようになるのは理想の世界かもしれない。で、そのためには非エンジニアでも読みやすいように RDRA のモデルを工夫して書く必要があるようです。Figma + Instance Finder はよさそう。ただ、個人の思考ツールとして使うならそこまで作り込む必要はないんだろうけど。

業務の本質は RDRA で表現しにくいっていうのは興味深い視点。あと、RDRA は広さと深さを自由自在に行き来できるっていうのがいいポイント。

以下、メモから抜粋。

RDRAはどう形作られたか?

www.slideshare.net

  • 要件を決められない
  • 要件定義の精度が悪い
    • 暫定仕様が増え続ける
    • システムに近いところの機能を定義している
  • 精度を高める
    • 整合性は合っているか, 網羅しているか
  • 見積もり根拠
  • 要件を容易に変更したい
  • 全体の共通認識を得たい
  • What と Why を表現する
    • How は表現しない
  • ビジネうルールを表現する
    • 業務フローの単位を決める
  • 精度の高い要件 = 仕様の足場
  • ビジネスルールは条件 → 状態モデルで表現する
  • ビジネスユースケースを洗い出す
    • BUCがわかると全体のボリュームがわかり計画が立てられる

RDRA導入後の要件定義の変化

www.slideshare.net

  • 業務設計チーム
    • 企画と開発の橋渡し
    • プロダクトマネージャー?
  • 改修案件のQCDを上げたい
  • RDRAモデルをマスタにする
    • 改修案件でマスタから引用する
    • マスタにフィードバックする
  • BUCをプロダクトの業務として定義する
    • プロジェクトごとの業務フローの粒度をズレを防ぐ
  • 業務フロー, 状態モデル
    • 日本語による曖昧さを回避する
  • 非エンジニアでもRDRAモデルが読みやすくなるように工夫する
    • 記法の統一, 可読性の改善, 補助資料, ...
  • ステークホルダとの合意形成ツール

新規サービス開発で RDRA を使っている話

  • spreadsheet
  • アクター, 外部システム, 情報の洗い出し
  • ユースケース洗い出し
    • アクティビティ, UC の抽出粒度が難しい
    • 業務の理解度によって粒度が変わりそう
    • ヒアリング
  • 業務とアクターの分析
    • 誰がどのような業務をやっているか
    • 抜け漏れが見つけられる
  • 情報/状態の構造化
    • 情報の関連を引く
    • バリエーションのリストアップ
  • 要求の洗い出し
    • 業務要求/非機能要求を整理する
  • IT要求の解像度を上げる
  • ToBe の全体イメージを早めに付ける
    • 必要に応じて AsIs を詳細化する
  • 情報モデルの検討
    • オブジェクトモデルがあるとよい
  • アクター, BUC 洗い出し
    • ユースケースを細かくしていくと状態やバリエーションが見つかる
    • RDRAモデルで別の形で表現できたり
  • 状態モデルの検討
  • ToBe モデルを書き進めると自然と BUC が精査されていく
  • 粒度が粗いところは理解が不足しているかもしれない
    • 現状分析が必要

RDRAと業務と私

  • ヒアリング/観察
  • 業務を理解することで要件定義ができるように
  • チームの業務理解度を上げる
  • 書かないと読めるようにならない
  • ユーザーがシステムを触っている目線で書く
    • エンジニア視点だと細かくなりがち
  • ユーザーと一緒にRDRAを書く (モブプロ)
  • Enterprise Architect
  • Figma + Instance Finder
  • RDRA + JIG
  • 業務の本質はRDRAで表現しにくい

RDRA2.0を1年半つかって実感した効果

  • RDRAでそれぞれの関係性が整理できる
  • データ系モデルが作りやすい
  • 見積もりの精度をあげるときは仕様化/設計する
  • 設計/実装にスムーズに入れる
  • 段階的詳細化
    • 抽象 → 具体
  • 広さと深さを自由自在に行き来できる
    • 広さ (要件のリスト)
    • 深さ (要件の詳細化)
  • ユーザーストーリーマッピングは広さを担保するのが大変
    • 1つのマップを深堀りするため

ドメイン駆動設計にRDRA2.0を活用する

  • ユースケースをちゃんと洗い出せているか → RDRA
  • 視点を増やす
  • ユースケース ← 業務フロー ← ビジネスユースケース (業務バリエーション)
  • 関係で考える
  • 事業活動のモデル (ビジネスコンテキスト)
    • 業務のバリエーション
    • ビジネスルールを漏れなく見つける
  • それぞれは完全に洗い出せていなくても、それぞれの関係から考えることで抜け漏れを見つけられる
  • ビジネスルールとドメインモデルを固める
  • 共通認識のツールとしてRDRAのモデルを仕上げる

その他

  • クロスリファレンスできるツールが欲しくなる
  • spreadsheet はみんなでやるときに早くていい
  • ビジネスユースケースの粒度を合わせたい
  • 他のモデルを書いていくと粒度が合っていく
    • 理解が進んでバランスが取れていく
  • ピクト図解
  • 非機能要件 > 要求モデル図に書く
  • モジュラーモノリスで作る
    • コンテキストが明確になってから切り出した方がよさそう
  • BUCは早く出す
    • アクティビティを洗い出す
    • グルーピングして名前を付けてBUCを出した方がよさそう
  • RDRA
    • 個人の思考ツールとして使う
      • 実装に移行して以降は JIG で見る
    • マスタとして使う
  • revise しやすく保つためには軽くしておくとよさそう

AWS SDK for Java で S3 のファイルをまとめてダウンロードしたい

S3 のフォルダにあるファイルを zip でまとめてダウンロードしたい。今回は AWS SDK for JavaAmazonS3#getObject を非同期で呼び出して ZipOutputStream で書き出してみる。

サンプルコードはこちら。

github.com

前提

備忘録

起動クラスに@EnableAsync アノテーションを付けて非同期処理を有効にする。

@EnableAsync
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
    // ...(略)

任意の prefix に一致するオブジェクトリストを取得する。サブフォルダは除く。

public List<String> listObjectKeys(String bucketName, String prefix) {
    return amazonS3.listObjectsV2(bucketName, prefix)
            .getObjectSummaries().stream()
            .filter(object -> !object.getKey().endsWith("/")) // サブフォルダは除く
            .map(object -> object.getKey())
            .collect(Collectors.toList());
}

AmazonS3#getObject を非同期で呼び出す。戻り値には CompletableFuture<T> を返す。ドキュメントを読む限りでは AmazonS3 はスレッドセーフっぽい。(AmazonS3ClientBuilder の方はスレッドアンセーフ)

サービスクライアントの作成 - AWS SDK for Java

@Async
public CompletableFuture<S3Content> fetchAsync(String bucketName, String objectKey) {
    S3Object object = amazonS3.getObject(new GetObjectRequest(bucketName, objectKey));
    return CompletableFuture.completedFuture(new S3Content(object));
}

オブジェクトリストの objectKey ごとに @Async な非同期処理を呼び出して処理完了を待つ。

List<S3Content> s3Contents = new ArrayList<>();
List<CompletableFuture<S3Content>> processes = new ArrayList<>();
for (String objectKey : objectKeys) {
    CompletableFuture<S3Content> process =
            service.fetchAsync(bucketName, objectKey).whenCompleteAsync((res, e) -> s3Contents.add(res));
    processes.add(process);
}
CompletableFuture.allOf(processes.toArray(new CompletableFuture[objectKeys.size()])).join();

byte 配列のデータを ZipOutputStream で書き出す。今回は ServletResponse#getOutputStream をラップして呼び出し元に返すようにする。

String basename = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));

response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + basename + ".zip");
response.setStatus(HttpServletResponse.SC_OK);

try (ZipOutputStream out = new ZipOutputStream(response.getOutputStream())) {
    for (S3Content content : s3Contents) {
        out.putNextEntry(new ZipEntry(Paths.get(basename, content.filename()).toString()));
        try (InputStream in = new ByteArrayInputStream(content.data())) {
            byte[] buf = new byte[4096];
            int len;
            while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
            }
        }
    }
}

Clipboard API

備忘録。

ある要素の click イベントで、Clipboard API を利用して要素内のテキストをクリップボードにコピーする。

document.querySelectorAll('.copy-to-clipboard').forEach((el)=> {
  el.addEventListener('click', (e) => {
    const text = e.currentTarget.textContent
    navigator.clipboard.writeText(text).then(() => {
      console.log('copied!!');
    });
  }, false)
})

モデリングの学び方:座談会 に行ってきた #modeling_zadankai

モデリングの学び方:座談会 に参加しました。簡単に所感をまとめます。

modeling-how-to-learn.connpass.com

所感

現場でどうモデリングをやっているか、モデリングをどう学んできたか、などいろいろな話が聴けて面白かったです。最後に言っていた「本をきちんと読むのはモデリングの基本スキル」というのが刺さりました。読解力。つまり Input を深く理解する力が大事。

あと、語彙のモデリング。用語集もモデリングのひとつと言えるのか、なるほど。

RDRA 2.0 ハンドブックはさらっとしか読んでないのでしっかり読み直したい。

今回の座談会にいた方の YouTube 動画。自分が若手のときにこういうコンテンツがあったらなぁ...。

youtu.be

youtu.be

以下、メモから抜粋。

モデリングの学び方:座談会

  • モデリング
  • 重要な要素, 重要な関係性, 適切な名前
  • 要点の発見 (言語化/可視化)
  • 事業活動のモデリング
  • ソフトウェアのモデリング
  • 語彙のモデリング
  • アプローチ
    • プロセス中心
    • データ中心
    • ルール中心
  • ドメインモデル中心
  • RDRA 2.0
  • モデルの改善 > データの変更 > マイグレーションどうする?
  • どういう事業をやっているか
    • ビジネスの理解
    • ビジネスを俯瞰する
  • コンテキストマップ
  • ビジネスの目的を達成するためのモデル
  • どういうビジネスをやっていて何をシステム化するのか
  • 用語, 関連, 多重度
  • command/query
    • command を処理するのに必要なデータしか持たせない
  • 問題があるモデルを改善する経験
  • 使えるモデル, 役に立つモデル
  • Tell, Don't Ask
    • やることを依頼する
    • message passing
    • Tell, Don't Ask しないならデータモデリングでいい
  • リファクタリングは設計
  • 本をきちんと読むのはモデリングの基本スキル

あとで読む。

JSUG勉強会2021年その2 Spring GraphQL をとことん語る夕べ に行ってきた #jsug

JSUG勉強会2021年その2 Spring GraphQL をとことん語る夕べ に参加しました。今回はオンライン開催。簡単に所感をまとめます。

jsug.doorkeeper.jp

所感

Spring GraphQL は内部的には graphql-java を使っているようです。昔、graphql-java と Spring Boot を試してみたことがあったっけ。

DataLoader, ページングのデモが分かりやすかったです。このあたりは実際に書いてみた方がより理解できそう。

M2 でマルチパート (ファイルアップロード) が対応されたら確かにおもしろそうだけど、これ本当に GraphQL で対応する必要あるんだろうか...。これこそ GraphQL に合わない API な気もしたりしなかったり。

以下、メモから抜粋。

Spring GraphQL introduction

https://backpaper0.github.io/spring-graphql-introduction/

  • Spring GraphQL 1.0.0-M1
  • Query, Mutation, Subscription
    • schema キーワードで型をカスタマイズできる
  • 1回のリクエストで複数のクエリを送信できる
  • バージョンなしでAPIを進化できる
    • 型にフィールドが追加されても影響ない
    • 削除には @deprecated ディレクティブを付ける
  • Spring GraphQL
    • M2 でクライアント側の対応が入るかも
    • Web MVC, WebFlux
  • subscription
    • spring-boot-starter-websocket
  • 型定義ファイルは分割可
  • GraphiQL
  • GraphQlTester
  • N+1 問題
  • DataLoader
  • BatchLoader
    • maxBatchSize (バッチサイズ設定可)
  • MappedBatchLoader
    • 戻り型が Map になる
  • ページング
  • DataFetcherExceptionResolver
    • 例外ハンドリング
  • GraphQL に合わない API が欲しいとき
    • RestController 書けばいい
  • 認証/認可
    • エンドポイントが1つなのでエンドポイントごとの認証/認可は不十分
    • @PreAuthorize, @Secured
  • メトリクス (actuator)
  • M2
    • Spring Data 統合 (Querydsl)
    • DataFetcher, DataLoader 登録の改善
    • マルチパート対応
  • https://github.com/backpaper0/spring-graphql-introduction

その他

ik.am

  • MyBatis Thymeleaf
    • Thymeleaf テンプレートで SQL を動的に生成できる
    • MyBatis 以外でも使える
  • JdbcTemplate 使うひとにはよさそう

AWS SDK for Java + KMS で S3 暗号化

備忘録。前回の続き。

AWS SDK for Java で署名付き URL 生成 - kntmr-blog

Key Management Service (KMS) と AWS SDK for Java で、クライアントサイドで暗号化して S3 にアップロードする。

キー作成 (CMS)

事前に Key Management Service > カスタマー管理型のキー でキーを作成する。今回はダウンロードして復号したいので、キーのタイプには 対称 を選択する。

AWS Encryption SDK

AWS Encryption SDK for Java - AWS Encryption SDK

暗号化に必要なライブラリを追加。これがないと AmazonS3EncryptionV2 を初期化する際に実行時エラーが発生する。

<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-encryption-sdk-java</artifactId>
    <version>2.0.0</version>
</dependency>

暗号化&アップロード

パターン1

AmazonS3EncryptionV2 s3Encryption = AmazonS3EncryptionClientV2Builder.standard()
        .withCredentials(new ProfileCredentialsProvider())
        .withCryptoConfiguration(new CryptoConfigurationV2()
                .withCryptoMode(CryptoMode.StrictAuthenticatedEncryption))
        .withEncryptionMaterialsProvider(new KMSEncryptionMaterialsProvider(keyId))
        .build();

try {
    s3Encryption.putObject(bucketName, objectKey, file);
} finally {
    s3Encryption.shutdown();
}

パターン2

SDK から KMS のキーを作成してアップロードするパターン。ついでにキーの削除スケジュールをリクエストする。ScheduleKeyDeletionRequest#withPendingWindowInDays にはキーが削除可能になるまでの待機日数を指定する。(7〜30)

AWSKMS kmsClient = AWSKMSClientBuilder.standard()
        .withCredentials(new ProfileCredentialsProvider())
        .build();

CreateKeyRequest createKeyRequest = new CreateKeyRequest();
CreateKeyResult createKeyResult = kmsClient.createKey(createKeyRequest);
String keyId = createKeyResult.getKeyMetadata().getKeyId();

AmazonS3EncryptionV2 s3Encryption = AmazonS3EncryptionClientV2Builder.standard()
        .withCredentials(new ProfileCredentialsProvider())
        .withCryptoConfiguration(new CryptoConfigurationV2()
                .withCryptoMode(CryptoMode.StrictAuthenticatedEncryption))
        .withEncryptionMaterialsProvider(new KMSEncryptionMaterialsProvider(keyId))
        .build();

try {
    s3Encryption.putObject(bucketName, objectKey, file);

    ScheduleKeyDeletionRequest scheduleKeyDeletionRequest = new ScheduleKeyDeletionRequest()
            .withKeyId(keyId)
            .withPendingWindowInDays(7);
    kmsClient.scheduleKeyDeletion(scheduleKeyDeletionRequest);
} finally {
    s3Encryption.shutdown();
    kmsClient.shutdown();
}

ダウンロード&復号

AmazonS3EncryptionV2 s3Encryption = AmazonS3EncryptionClientV2Builder.standard()
        .withCredentials(new ProfileCredentialsProvider())
        .withCryptoConfiguration(new CryptoConfigurationV2()
                .withCryptoMode(CryptoMode.StrictAuthenticatedEncryption))
        .withEncryptionMaterialsProvider(new KMSEncryptionMaterialsProvider(keyId))
        .build();

try {
    GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, objectKey);
    s3Encryption.getObject(getObjectRequest, file);
} finally {
    s3Encryption.shutdown();
}

その他

暗号化したときの keyId 以外を指定した場合、AmazonS3EncryptionV2#getObject でエラーが発生する。(status code 400)

com.amazonaws.services.kms.model.IncorrectKeyException: The key ID in the request does not identify a CMK that can perform this operation.

暗号化してアップロードしたファイルは AmazonS3#getObject でもダウンロードできるが、暗号化されているため開けない。マネジメントコンソールからダウンロードした場合も同様に開けない。

暗号化したファイルのコピーや削除では keyId の指定は不要で、AmazonS3#copyObject or AmazonS3.deleteObject が使える。もちろん、暗号化したままコピーされる。

AWS SDK for Java で S3 署名付き URL 生成

備忘録。S3 の 署名付き URL を AWS SDK for Java で生成する。AWS の設定周りについては正しいかどうかは自信がない...。

<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-s3 -->
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-s3</artifactId>
    <version>1.11.1034</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-sts -->
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-sts</artifactId>
    <version>1.11.1034</version>
</dependency>

パターン1

IAM ユーザーの認証情報 (accessKey, secretKey) を利用して生成するパターン。

AmazonS3 amazonS3 = AmazonS3ClientBuilder.standard()
        .withCredentials(new ProfileCredentialsProvider())
        .build();

Date expiration = new Date();
expiration.setTime(Instant.now().toEpochMilli() + 1000 * 60 * 5); // 有効期限5分
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, objectKey)
        .withMethod(HttpMethod.GET) // GET のみ許可する
        .withExpiration(expiration);

URL presignedUrl = amazonS3.generatePresignedUrl(request);

パターン2

AWS STS で一時的なセキュリティ認証情報を取得して生成するパターン。事前に AmazonS3ReadOnlyAccess をアタッチしたロールを作成する。また、「信頼関係の編集」で sts:RoleSessionName を追加する。

... (略)
"Condition": {
  "StringLike": {
    "sts:RoleSessionName": "${aws:username}"
  }
}
AWSSecurityTokenService stsClient = AWSSecurityTokenServiceClientBuilder.standard()
        .withCredentials(new ProfileCredentialsProvider())
        .build();

AssumeRoleRequest roleRequest = new AssumeRoleRequest()
        .withRoleArn(roleArn)
        .withRoleSessionName(roleSessionName);
AssumeRoleResult roleResult = stsClient.assumeRole(roleRequest);
Credentials sessionCredentials = roleResult.getCredentials();

BasicSessionCredentials awsCredentials = new BasicSessionCredentials(
        sessionCredentials.getAccessKeyId(),
        sessionCredentials.getSecretAccessKey(),
        sessionCredentials.getSessionToken());

AmazonS3 amazonS3 = AmazonS3ClientBuilder.standard()
        .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
        .build();

Date expiration = new Date();
expiration.setTime(Instant.now().toEpochMilli() + 1000 * 60 * 5); // 有効期限5分
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, objectKey)
        .withMethod(HttpMethod.GET) // GET のみ許可する
        .withExpiration(expiration);

URL presignedUrl = amazonS3.generatePresignedUrl(request);

その他

パターン2では URL に X-Amz-Security-Token=... パラメータが付く。このパラメータで一時的なセキュリティ認証情報を渡す。

ちなみに、マネジメントコンソールの「開く」でも署名付き URL は生成できる。有効期限は5分。(X-Amz-Expires=300)