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

短納期でローンチした新サービスをJavaで開発した話/launched new service using Java

A1
June 17, 2022

短納期でローンチした新サービスをJavaで開発した話/launched new service using Java

JJUG CCC 2022 Spring 発表資料

https://fortee.jp/jjug-ccc-2022-spring/proposal/3bf78003-4672-457c-a8fc-47adb4a1812e

概要 / Abstract:
電子帳簿保存法の改正により急遽立ち上がった新規サービス開発。
久々の新規開発でテックリードの腕の見せ所とはりきる私。
新しい技術要素をモリモリ使って開発したい..ただ法改正がトリガーなので絶対に納期が延ばせないが考えなければいけないことは盛り沢山
・使用するJavaのバージョンとフレームワーク
・マルチテナントDB方式
・APIクライアント
・セキュリティ関連
・多言語、タイムゾーン対応
・フロントエンド
・クラス設計の方針
などなど
そのような状況の中でJavaを中心とした技術選定で妥協しなかったことや開発で苦労したことなど、開発事例をお話させていただきます。

A1

June 17, 2022
Tweet

More Decks by A1

Other Decks in Programming

Transcript

  1. • はじめに
 • 開発体制
 • Javaのバージョン
 • フレームワーク
 • APIクライアント


    • マルチテナント構成
 • セキュリティ
 • タイムゾーン対応
 • フロントエンド
 • クラス設計
 • ログ
 • おわりに
 3
 目次

  2. • はじめに
 • 開発体制
 • Javaのバージョン
 • フレームワーク
 • APIクライアント


    • マルチテナント構成
 • セキュリティ
 • タイムゾーン対応
 • フロントエンド
 • クラス設計
 • ログ
 • おわりに
 4

  3. • はじめに
 • 開発体制
 • Javaのバージョン
 • フレームワーク
 • APIクライアント


    • マルチテナント構成
 • セキュリティ
 • タイムゾーン対応
 • フロントエンド
 • クラス設計
 • ログ
 • おわりに
 7

  4. 開発体制
 既存サービスを開発している2チームが開発
 8
 アルファ・チーム 6名 ブラボー・チーム 7名 Tech Lead(me)
 PM


    PL/PdM
 基盤部分を実装するの 1人じゃ無理!! ※実際のチーム名とは異なります

  5. 基盤チーム 5名 開発体制
 一時的に基盤チームを立ち上げ
 9
 アルファ・チーム 4名 ブラボー・チーム 5名 Tech

    Lead(me)
 PM
 PL/PdM
 開発チームからメンバーを借りて 
 WG的なチームで基盤部分を一気に作り上げた 
 大凡タスクが完了したら開発チームに人を戻した 

  6. 開発体制
 「チームトポロジー」という本にこのようなチーム分けの話がありました。
 (最近出た本なので読んだのは開発後ですが)
 10
 ❖ ストリームアラインドチーム
 ➢ 顧客に直接価値を届けるいわゆる開発チーム ❖ プラットフォームチーム


    ➢ インフラやツール、共通サービスなどを提供するチーム ❖ イネイブリングチーム
 ➢ 他のチームが技術やスキルを身につけるのを支援するチーム ❖ コンプリケイテッド・サブシステムチーム
 ➢ 複雑なサブシステムやコンポーネントを扱う専門チーム • 図らずも今回の基盤チームはプラットフォームチームとイネイブリン グチーム的な役割を担っていた
 • 現在、組織が拡大してチーム数が増えていっている状況の中、どう やって新しい技術を広めていくか悩んでいるところだったので非常に 参考になりました
 チームトポロジー
 価値あるソフトウェアをすばやく届ける適応型組織設計
 マシュー・スケルトン 著 マニュエル・パイス 著

  7. • はじめに
 • 開発体制
 • Javaのバージョン
 • フレームワーク
 • APIクライアント


    • マルチテナント構成
 • セキュリティ
 • タイムゾーン対応
 • フロントエンド
 • クラス設計
 • ログ
 • おわりに
 11

  8. Javaのバージョン
 17の良かった新機能☺ ※Java11との比較です
 • Record
 19
 /** * recordで書いた場合 */

    public record LoginId(long value) { public LoginId { if (value <= 0L) { throw new IllegalArgumentException(); } } } Immutableなクラスが短く書けて便利なのでValue Objectの実装で多用した。
 
 /** * classで書いた場合(今までの書き方) */ public class LoginId { private final long value; public LoginId(long value) { if (value <= 0L) { throw new IllegalArgumentException(); } this.value = value; } public long getValue() { return this.value; } }
  9. Javaのバージョン
 もしかしてrecordがあればlombokは不要では?🤔
 というチームメンバーから疑問の声が
 →そんなことはなかった
  確かに@Valueは使わないかもしれないが他のメソッドは使いたい
 20
 アノテーション 用途 @Slf4j Logger

    log = org.slf4j.LoggerFactory.getLogger(...);のショートハンド。 めちゃくちゃ使う @AllArgsConstructorなどコンストラクター系 コンストラクタでDIをインジェクションしたいのでめちゃくちゃ使う @NonNull 引数の必須チェックなどに使用する
  10. Javaのバージョン
 Java17の良かった新機能
 21
 • テキストブロック
 ◦ 個人的には長年待ち望んでいた機能 ◦ SQLを文字列に書いたりする機会が減って思ったよりは使用ケースは少なかった ◦

    テストコードではかなり使用した mockMvc.perform(MockMvcRequestBuilders.put("/user") .content("""{ "id": 1, "name": "Taro Yamada", "age": 23 }""").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().json(expected, true))
  11. Javaのバージョン
 Java17の地味だけど良かった機能
 • stream.toList
 23
 List<String> list = List.of("", "a");

    List<String> list2 = list.stream().filter(a -> !a.isEmpty()).toList(); collect(Collectors.toList())は長いし直感的じゃないのですごく便利
 • NullPointerExceptionのエラーメッセージ改善
 String s = getS(); getNull(s.toUpperCase()).getBytes(); Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "s" is null at BasicCompletionDemo.main(Learning.java:7) 数珠つなぎに書いてたりすると、どこがnullなのか分からないことがあったが解消されている

  12. • はじめに
 • 開発体制
 • Javaのバージョン
 • フレームワーク
 • APIクライアント


    • マルチテナント構成
 • セキュリティ
 • タイムゾーン対応
 • フロントエンド
 • クラス設計
 • ログ
 • おわりに
 24

  13. フレームワーク
 結論:Spring Bootを選択した
 ◦ 現状、特殊な事情がない限りSpring Bootで良さそう ◦ 既存サービスではSpring MVCなのでbootJarがとても楽に感じた ▪

    Tomcatの面倒をインフラにお願いしないで済むのもとても良い ◦ ヘルスチェックを実装しなくて良いのでActuatorが便利だった。 
 25

  14. フレームワーク
 • Javaの性能監視にはJolokiaを使用
 ◦ jmxのエンドポイントの設定が不要 ◦ APIでJSONを取得するスタイルなので汎用性が高い • Spring Actuatorが対応しているので、機能を有効にしてjolokia-coreを依存関

    係に追加するだけで使用可能で便利だった。
 
 
 
 • デフォルトでは組み込みTomcatのMbeanが取得できないので下記設定を追 加しました
 26
 # Jolokiaでメトリクス取得するため、TomcatのMbeanを取得可能にする server.tomcat.mbeanregistry.enabled: true # healthとjolokiaを有効に management.endpoints.web.exposure.include: "health,jolokia"
  15. • はじめに
 • 開発体制
 • Javaのバージョン
 • フレームワーク
 • APIクライアント


    • マルチテナント構成
 • セキュリティ
 • タイムゾーン対応
 • フロントエンド
 • クラス設計
 • ログ
 • おわりに
 27

  16. APIクライアント
 • SpringのRestTemplateを使おうと思ったが、調べてみると今後はメンテナンスモードに なるということだったので比較検討しました
 ◦ 真っ先に上がる候補はSpringのWebClientですが、このためだけにWebFluxを入 れたくないので不採用とした ◦ 結局、OKHttp(Retrofit)を採用した 


    • OkHttp, Retrofitとは
 ◦ OkHttpはHTTP通信に特化した軽量なライブラリでAndroidでよく使われているみた いです。 ◦ RetrofitはOkHttpをラップしてより便利に使えるようにしたライブラリです。 ◦ 去年のアドベントカレンダーで3本記事を書きました 28
 Retrofit+OkHttpでAPIエラーの時のレスポンスの取得ではまった Retrofit+OkHttpでmultipart/form-dataなAPIの呼び出してハマった Retrofit+OkHttpでファイルダウンロードするAPIを呼び出す
  17. APIクライアント
 29
 interface UsersApiService { @GET("{tenantId}/users" ) Call<ResponseData> userList(@Path("tenantId") int

    tenantId, @Query("sort") String sort); } // Responseを詰めるクラスも作っておきます record ResponseData(String status, Integer code, List<User> userList){}; record User(String name, String mailAddress){}; interfaceにAPIごとにメソッドを宣言します。 
 上記の例はGETで {tenantId}/users?sort={sort} を呼び出してレスポンスボディをResponseData型に詰めるという定義です 
 Retrofit retorofit = new Retrofit.Builder() .baseUrl("http://localhost:8080" ) // JSONを変換するコンバーターを指定(今回は Jacksonを使用) .addConverterFactory(JacksonConverterFactory.create()) .build(); // 先ほど作成したinterfaceからインスタンスを生成 UsersApiService service = retorofit.create(UsersApiService. class); // API実行! ResponseData data = service.userList(1, "asc").execute().body(); あとは、下記のように呼び出します。UserApiServiceのメソッド呼び出しは普通のJavaのクラスを呼ぶような感覚で呼び 出すことができます。 • Hello World

  18. • はじめに
 • 開発体制
 • Javaのバージョン
 • フレームワーク
 • APIクライアント


    • マルチテナント構成
 • セキュリティ
 • タイムゾーン対応
 • フロントエンド
 • クラス設計
 • ログ
 • おわりに
 30

  19.  低                                                 マルチテナント構成
 高
 32
  高                                                 A. データベース分離方式 B. 単一データベース・個別スキーマ方式 C.

    単一データベース・共通スキーマ方式 初期構築コスト
 運用コスト
 —----- -------- —----- -------- —----- -------- —----- -------- —----- -------- —----- -------- —----- -------- —----- -------- —----- -------- —----- -------- —----- -------- —----- -------- —----- -------- —----- -------- マルチテナント構成の実現方法
 低

  20. マルチテナント構成
 34
 CREATE POLICY isolation_policy ON [table_name] USING (tenant_id =

    current_setting('app.current_tenant_id')::bigint); • PostgreSQLのRow Level Security(RLS)で他テナントのレコードを間違って検索しないことを担 保
 ◦ 万が一SQLでtenant_idでの絞り込みを間違っても、app.current_tenant_idに指定した以外 のデータは取得できないことが担保できる
  21. マルチテナント構成
 35
 • app.current_tenant_idの設定はトランザクションごとに設定するようにspringの DataSourceTransactionManagerを拡張
 public class MyTransactionManager extends DataSourceTransactionManager

    { @Override protected void prepareTransactionalConnection (Connection con, TransactionDefinition definition) throws SQLException { super.prepareTransactionalConnection(con, definition); try (Statement stmt = con.createStatement()) { stmt.execute("SET local app.current_tenant_id = " + TenantContextHolder .getTenantId()); } } } • ここまで仕組みを作ったのでこれで安心できた
 • RLSはSaaSの業界で最近流行っているようで沢山事例が公開されていて参考になりました

  22. • はじめに
 • 開発体制
 • Javaのバージョン
 • フレームワーク
 • APIクライアント


    • マルチテナント構成
 • セキュリティ
 • タイムゾーン対応
 • フロントエンド
 • クラス設計
 • ログ
 • おわりに
 36

  23. セキュリティ
 • 認証やCSRFチェックはSpring Securityを使用
 ◦ デフォルトで必須級のセキュリティ系のヘッダーを付与してくれるので楽だった 37
 Cache-Control: no-store, no-cache,

    must-revalidate, max-age=0 Pragma: no-cache Expires: 0 X-XSS-Protection: 1; mode=block X-Frame-Options: DENY X-Content-Type-Options: nosniff Strict-Transport-Security: max-age=31536000; includeSubDomains
  24. セキュリティ
 38
 不足するヘッダーだけ自分で設定した Content-Security-Policyはユーザが沢山いる状態で入れるのは大変なので早めに入れられて良かった Referrer-Policy "strict-origin-when-cross-origin" Content-Security-Policy "default-src 'self'; connect-src

    'self' https://www.google-analytics.com; img-src 'self' https://www.google-analytics.com; script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/; font-src https://fonts.googleapis.com/ https://fonts.gstatic.com/; child-src 'none'; object-src 'none'" 「体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生 まれる原理と対策の実践」(徳丸本)は必読ですが、 ちょっと前に出た「Webブラウザセキュリティ」という本もとても参考になり ました。 Webブラウザセキュリティ ― Webアプリケーションの安全性を支える仕組みを整理する 米内貴志 著

  25. セキュリティ
 • 認証処理を組み込んだところ性能問題が発生
 ◦ 計測したらログインに2秒近く掛かっていた。 ◦ プロファイルを取ってみるとBCryptPasswordEncoderが遅いことが分かった ◦ 社内のセキュリティ要件に従うためstrengthに14(16384回ストレッチ)を指定したのが原因 ▪

    この件をQiitaにまとめて下さっている人がいました • JavaでBCrypt + ストレッチ回数ごとの処理時間計測 • デフォルトの1024回ストレッチでも100ms以上掛かっているのでそもそもコストが高い処理 ◦ BCryptPasswordEncoderを使用せずに自前実装に切り替えたところ1500msから350msまで改善 しました ▪ なんでこんなに違いが出るのかは細かいことは分かっていません。 39

  26. • はじめに
 • 開発体制
 • Javaのバージョン
 • フレームワーク
 • APIクライアント


    • マルチテナント構成
 • セキュリティ
 • タイムゾーン対応
 • フロントエンド
 • クラス設計
 • ログ
 • おわりに
 40

  27. タイムゾーン対応
 • ファーストリリースでは日本国内での使用のみ想定すれば良いのだが、将来 的に海外対応が必要になる確度が高いので最初からTimezoneありきで実装を 行った。
 ◦ LocalDatetimeで実装して後から海外対応しようとすると修正箇所が多すぎて辛い ◦ ただし、夏時間を考慮しようとすると途端に難しくなるのでここでは考慮しない 41


    APサーバ ユーザの入力値 :2022-05-10 10:00:00 2022-05-10 T01:00:00+00 ZonedDateTime: 2022-05-10 10:00:00(Asia/Tokyo) OffsetDateTime: 2022-05-10 01:00:00+00 DB
 とりあえずAsia/Tokyo固定 
 でZonedDateTimeで受け取る 
 OffsetDateTime(UTC)に変換して 処理する
 DBにはUTCで格納 
 ※データの取得は逆の順番で同じように行う 

  28. タイムゾーン対応
 42
 • PostgreSQLの timestamp with time zone型について
 timestamp with

    time zoneについて内部に格納されている値は常に UTCです(協定世界時、歴史的にグリニッジ標準時 GMTとして知られています)。 時間帯が明示的に指定された入力値は、その時間帯に適したオフセットを使用して UTCに変 換されます。 入力文字列に時間帯が指定されていない場合は、システムの TimeZoneパラメータに示されている値が時間 帯とみなされ、timezone時間帯用のオフセットを使用して UTCに変換されます。 (公式ドキュメントより引用) 初めて使いましたが、下記の点がちょっと意外でした
 • 時間帯を明示しないと実行環境のTimezoneへの変換が自動で行われる
 • 名前に反してtimezone情報は保持しない。常にUTCに変換して保存される
 ◦ 確かにtimezoneあり/なし、どちらも格納サイズは8バイトで違いはない
  29. タイムゾーン対応
 43
 app=# set timezone to 'Asia/Tokyo'; タイムゾーンをAsia/Tokyoにセットする(大抵の場合デフォルトこれ) SET app=#

    insert into test values(2, '2022-05-09 16:47:29'); 日本時間(+0900)の2022-05-09 16:47:29としてインサート INSERT 0 1 app=# select * from test; id | start_time ----+------------------------ 2 | 2022-05-09 16:47:29+09 日本時間(+0900)で取得される。 (1 row) • SQLクラインアントから実行すると、自動的に日本時間に変換してくれるの でデバッグや運用でのDB操作は問題なさそう

  30. • はじめに
 • 開発体制
 • Javaのバージョン
 • フレームワーク
 • APIクライアント


    • マルチテナント構成
 • セキュリティ
 • タイムゾーン対応
 • フロントエンド
 • クラス設計
 • ログ
 • おわりに
 45

  31. フロントエンド
 • 既存サービスを踏襲して、今回のサービスでもReactを採用
 ◦ サーバー側はJSONを返却するAPIを実装する ◦ I/Fだけ固めてしまってフロント側は基本的にお任せ ◦ サーバ側だけでも覚えることが多くなってきており責任分界点ができて良い 


    • フロントエンドについてはJJUG CCC 2021 Springで同僚が話をしていますの で良ければご覧ください
 ◦ 「フロントエンド・バックエンド分離の道のり」 ◦ https://fortee.jp/jjug-ccc-2021-spring/proposal/a0cc4346-f001-44dc-8df4-5974ce40b767 ◦ https://www.youtube.com/watch?v=1b3riDczwkQ 
 46

  32. • はじめに
 • 開発体制
 • Javaのバージョン
 • フレームワーク
 • APIクライアント


    • マルチテナント構成
 • セキュリティ
 • タイムゾーン対応
 • フロントエンド
 • クラス設計
 • ログ
 • おわりに
 47

  33. クラス設計
 Springのstereotypeを使って素直に実装すると3層アーキテクチャになる
 
 48
 @Controller @Service @Repository ビュー: JSPなど プレゼンテーション層(UI層):


     表示、入力内容受け取り、入力内容のチェック、
  UIに合わせた形式変換
 ビジネスロジック層(ドメイン層):
  業務処理を実装する
 データアクセス層(インフラ層):
  データベースとやりとりを実装する

  34. クラス設計
 • 3層アーキテクチャのメリット
 ◦ いわゆる「トランザクションスクリプト」に処理をつらつら書いて行けばやりたいことが実現できる(オブ ジェクト指向を分かってなくていい) ◦ クラス設計にコストが掛からないので高速に開発できる ◦ ユースケース単位で分担して実装すれば良いので平行して作業しやすい

    
 • 3層アーキテクチャのデメリット
 ◦ 処理の共通化はその人任せになり同じ処理が重複して書かれがちになる ◦ 修正の影響がどこに出るのか探しにくい ◦ 影響があちこちにでてしまう 
 デメリットは既存サービスで味わってきた。
 長期メンテナンスには辛い構造なので時間がなくても今回のサービスでは採用しない
 49

  35. クラス設計
 デメリットはビジネスロジック層に集中しているのでテコ入れする
 50
 @Controller @Service @Repository ビュー: JSPなど プレゼンテーション層(UI層):
  表示、入力内容受け取り、入力内容のチェック、


     UIに合わせた形式変換
 ビジネスロジック層(ドメイン層):
  業務処理を実装する
 データアクセス層(インフラ層):
  データベースとやりとりを実装する
 この層を細分化す るルールを作成す れば良さそう🤔
  36. クラス設計
 オニオンアーキテクチャを採用した
 51
 アプリケーション層 ドメイン層 インフラストラクチャ層 ユーザインターフェース層 アプリケーション層 ドメイン層 インフラストラクチャ層

    ユーザインターフェース層 レイヤードアーキテクチャ
 オニオンアーキテクチャ
 3層アーキ テクチャのビ ジネスロジッ ク層が分離 された
 ドメイン層
 アプリケーション層 
 ユーザインターフェース層 
 インフラストラクチャ層 
 丸で表現するとタマネギみたい な形になるのが由来
 この依存度 の向きが逆
 他にもヘキサゴナルアーキテクチャやクリーンアーキテクチャがあるがそこまで複雑なものは必要なくオニオンアーキテクチャがちょうど 良かった

  37. クラス設計
 53
 • とは言え、クラスを設計すること自体に慣れてなく苦戦は必至だった
 ◦ だいぶ前から読書会や勉強会を主催、良記事の紹介など地道な啓蒙活動を行った ◦ 既存サービスで部分的に実践 ◦ 理解を深めるため何冊も本を読みました

    エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践) エリック・エヴァンス著
 ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本 成瀬 允宣 著
 ドメイン駆動設計 モデリング/実装ガイド 松岡幸一郎 著
 ドメイン駆動設計 サンプルコード&FAQ 松岡幸一郎 著
 セキュア・バイ・デザイン Dan Bergh Johnsson, Daniel Deogun, Daniel Sawano 著

  38. • はじめに
 • 開発体制
 • Javaのバージョン
 • フレームワーク
 • APIクライアント


    • マルチテナント構成
 • セキュリティ
 • タイムゾーン対応
 • フロントエンド
 • クラス設計
 • ログ
 • おわりに
 54

  39. ログ
 • AbstractRequestLoggingFilterを参考にContentCachingRequestWrapper、 ContentCachingResponseWrapperを使用するように実装しました
 57
 @AllArgsConstructor public class RequestAndResponseLoggingFilter extends

    OncePerRequestFilter { @Override protected void doFilterInternal (@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) { doFilterWrapped(wrapRequest(request), wrapResponse(response), filterChain); } private void doFilterWrapped (ContentCachingResponseWrapper request, ContentCachingResponseWrapper response) { try { // リクエストログ出力 (予め指定してキーに絞ってログ出力) writeRequestLog(request); // 処理の実行 long start = System.currentTimeMillis(); filterChain.doFilter(request, response); long latency = System.currentTimeMillis() - start; // レスポンスログ出力 (予め指定してキーに絞ってログ出力) writeResponseLog(response , latency); } finally { response.copyBodyToResponse(); // レスポンスに書き込み } } private ContentCachingResponseWrapper wrapRequest(HttpServletRequest request) { return new ContentCachingResponseWrapper (request); } private ContentCachingResponseWrapper wrapResponse(HttpServletResponse response) { if (response instanceof ContentCachingResponseWrapper wrapper) { return wrapper; } else { return new ContentCachingResponseWrapper(response); } } }  ※メモリ使用量は増えると思いますので、ご注意を。

  40. 他にも沢山のタスクをこなす必要がある...
 • パブリッククラウド or オンプレ検討
 • コンテナ or VM or

    物理サーバ
 • システム構成検討
 • スケールアウト方式検討(APP/DB)
 • バックアップ・リストア検討
 • ドメイン取得
 • URL設計
 • SSL証明書取得(暗号化方式, TLSバージョン)
 • ログ設計(syslogの設計)
 • CI設定
 • systemd設定
 • サーバのディレクトリ構成設計
 • オブジェクトストレージ設計
 • 各種タイムアウト値設計
 • リクエストサイズの上限設定 (servlet.multipart.maxFileSize, servlet.multipart.maxRequestSize)
 • Cookie設計
 • セッション設計
 • LBの設定(Sticky Session)
 • PostgreSQLの設定値検討
 • PostgreSQLのスキーマ設計
 • PostgreSQLのRole設計
 • コネクションプーリング検討
 58
 • JVMパラメータ設計(ヒープサイズ、Metaspaceなど)
 • GC設計(GC種類の検討、GCログなど)
 • 性能測定
 • パッケージ設計
 • 使用するMWのライセンス確認
 • 排他制御の方式検討
 • ウィルススキャン検討
 • コーディング・DB規約作成
 • バッチ設計
 • 制御文字チェック(nullやnbspなどバイト列)
 • メール設計(spf、dkim、smtp、メールヘッダーの検討など)
 • メールテンプレート設計(テンプレートエンジンは何を使うかなど)
 • TomcatやSpringのエラー画面を表示させない対応
 • ステージング設計
 • ヘルスチェック設計
 • APM検討
 • 文字コード検討
 • クローラー対策
 • 脆弱性チェック
 • 運用設計(スレッドダンプの取得方法の検討など)
 • メッセージ設計(他言語対応しやすいようにベタ書き禁止)
 • ORマッパーの検討
 • Reactのためのmod_rewrite設定