NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    新人アプリケーションエンジニアがマイクロサービスアーキテクチャを構築してみた【 Java × Python 】

    本記事は  ブログ書き初めウィーク  5日目の記事です。
    📝  4日目  ▶▶ 本記事 ▶▶  6日目  📅

    はじめに

    こんにちは、新入社員の大澤です。
    部署に配属されて半年近くが経過しましたが、現在は Java(Spring Boot)を使用したシステムの開発を担当しています。
    私が担当しているシステムではマイクロサービスアーキテクチャという構成を採用しています。
    これまでマイクロサービスアーキテクチャについて知らなかったのですが、業務で扱って学んでいく中で非常に面白い構成だと感じたので、実際にマイクロサービスアーキテクチャを採用した簡易的なアプリケーションを作成して構成について理解を深めていこうと思います。

    マイクロサービスアーキテクチャとは?

    マイクロサービスとは複数の分割されたシステムで構成されたアーキテクチャのことです。
    対になる用語としてモノリシックアーキテクチャがありますが、この構成では大きな1つのシステムの中に複数の機能・サービスが存在しています。 システムを機能・サービスごとに分割して独立したモジュールを構築し、モジュール間のやり取りによって機能させる構成がマイクロサービスアーキテクチャです。

    モノリシックアーキテクチャとマイクロサービスアーキテクチャの構成の違い

    マイクロサービスアーキテクチャのメリット

    マイクロサービスアーキテクチャを採用するメリットはいくつかあります。

    1. 技術選定が柔軟にできる
      機能・サービスがそれぞれ独立しているため、環境が異なるシステム同士の連携が可能です。
      そのため、サービスごとに適した技術を採用することができます。

    2. スピーディな開発
      各サービスが独立しているため、担当サービスごとの開発チームで独立した開発が可能です。 そのため、開発効率を上げることができます。

    3. 耐障害性
      モノリシックアーキテクチャの場合は一つのシステムで構成されているため障害が発生してしまうと全体に影響してしまうことがあります。
      しかし、各サービスが独立してるため、ある一つのサービスに障害が発生してもその影響範囲を最小限に抑えることができます。

    マイクロサービスアーキテクチャについてのメリットを述べましたが、実現したいシステムによって適しているアーキテクチャは異なるので、他のアーキテクチャのメリットも考慮しながら採用するアーキテクチャを決めていくことが重要です。

    実装するアプリケーションの構成

    今回は簡易的なものでマイクロサービスアーキテクチャを構築していきます。
    簡易的なものなので構成はコントローラーモジュールと1つのサービスモジュールにし、モジュール同士のやりとりは API で実現します。
    実際にマイクロサービスアーキテクチャを構築する際はサービスモジュールを増やし、サービスごとにDBを持つことで1つのマイクロサービスアーキテクチャが構成できます。
    あくまで「システムを機能ごとにコントローラとサービスに切り離すことができ、切り離したモジュール間の通信が簡単に実現可能」ということを実感できたらと思います。

    簡易アプリケーションの概要

    今回は独立したモジュールということをわかりやすくするために技術(言語)が異なるもの同士でやり取りを行います。
    言語は コントローラーに Java(Sprng Boot)、サービスに Python(Flask)を採用し、 REST API を使用してモジュール間の通信を実現します。
    画面部分は Spring Boot との組み合わせでよく採用される Thymeleaf を利用します。 また、モジュールを分けるメリットをより感じるために Python が得意な処理だけを Python に任せようと思います。
    Python は自然言語処理にもよく使用される言語なので、自然言語処理の一つである感情分析をさせてみましょう。
    構築するアプリケーションの具体的な機能の流れは以下のようになります。

    1. ユーザがブラウザでアプリケーションにアクセスし、感情分析させたい文章を入力
    2. 入力された文章を Java が受け取って Python 側の API にリクエストを送信
    3. Python は API で渡された文章を感情分析し、 結果を JSON 形式で レスポンスとして Java に返す
    4. Java はレスポンスをオブジェクトとして受け取り、結果を処理する
    5. Java が処理結果をテンプレート(Thymeleaf)に渡し、処理結果の画面を動的に生成させる
    6. ユーザのブラウザ上に処理結果の画面が表示される

    より詳細に機能の流れを表したものが以下の図です。

    実装するアプリケーションの処理の流れ

    Java の 実装

    ここから実際に実装していきます。
    バージョンは Java 23.0.1、Spring Boot 3.4.0 を使用します。
    Spring Boot 側の src 以下のフォルダ構成は以下のようになっています。

    .
    └── src/
        └── main/
            ├── java/
            │   └── com.example.apiApp/
            │       ├── api/
            │       │   └── ApiClient.java
            │       ├── controller/
            │       │   └── AppController.java
            │       ├── form/
            │       │   └── TextForm.java
            │       ├── model/
            │       │   ├── ApiResponse.java
            │       │   └── Sentiment.java
            │       ├── servise/
            │       │   └── SentimentService.java
            │       └── Application.java
            └── resources/
                ├── static/
                │   └── style.css
                ├── templates/
                │   └── index.html
                └── application.properties

    Controller の作成

    作成した Controller のコードは以下になります。
    AppController.java

    package com.example.app.controller;
    
    import com.example.app.api.ApiClient;
    import com.example.app.form.TextForm;
    import com.example.app.model.ApiResponse;
    import com.example.app.service.SentimentService;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Controller;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.servlet.ModelAndView;
    
    @Controller
    @RequiredArgsConstructor 
    public class AppController {
       
        final ApiClient client;
    
        @GetMapping("/")
        public ModelAndView index(ModelAndView mav) {
            // TextForm をテンプレートに渡してユーザが入力した値を格納させる
            mav.addObject("textForm", new TextForm());
            //表示する HTML ファイルの名前をセット
            mav.setViewName("index");
            return mav;
        }
    
        @GetMapping("/sentiment")
        public ModelAndView createResult(@ModelAttribute("textForm") @Validated TextForm textForm, //
                                         BindingResult result, //
                                         ModelAndView mav  //
        ) {
            // 入力された値が空の場合
            if (result.hasErrors()) {
                mav.addObject("textForm", textForm);
                //表示する HTML ファイルの名前をセット
                mav.setViewName("index");
                return mav;
            }
            // 感情分析する文章を Python に渡して結果を取得
            ApiResponse response = client.getSentiment(textForm.getInputText());
            // 取得した分析結果から感情を決定して格納
            String resultType = SentimentService.createResult(response);
            // "result" という名前テンプレート側に渡す
            mav.addObject("result", resultType);
            //表示する HTML ファイルの名前をセット
            mav.setViewName("index");
            return mav;
        }
    }
    

    前提として、この Spring Boot アプリケーションは ポート番号 8080 でローカルで起動します。
    その後の処理の流れは以下のようになります。
    ・ localhost:8080/ にアクセスすると index.html が表示される。
    ・ 表示された画面でテキストを入力し、送信ボタンを押すと localhost:8080/sentiment にアクセスする。
    ・ localhost:8080/sentiment にアクセスすると、 ApiClient の getSentiment() が Python にAPI リクエストを送る。

    API クライアントの作成

    Python 側のサービス に API を送信するクライアントとなるクラスです。
    API クライアントのコードは以下になります。
    ApiClient.java

    package com.example.app.api;
    
    import com.example.app.model.ApiResponse;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    
    @FeignClient(name = "apiApp", url = "localhost:5000/")
    public interface ApiClient {
        // localhost:5000/sentiment に引数の値をパラメータとした GET リクエストを送信する。
        // レスポンスを RestApiResponse 型のオブジェクトに格納
        // @FeignClient を付与することでメソッドを定義するだけで API クライアントが完成
        @GetMapping("/sentiment")
        ApiResponse getSentiment(@RequestParam("text") String text);
    }
    

    この ApiClient は getSentiment() が呼ばれると localhost:5000/sentiment にリクエストを送信します。 localhost:5000/sentiment は後述の Python のアプリケーションをローカルで実行した場合の URL です。 getSentiment() はメソッドの引数に指定された値をパラメータとして GET リクエストを送信します。

    その他のクラス

    アプリケーションの実装で作成したその他のコードは以下になります。

    • Form (ユーザが入力した内容を保持するクラス)

    ここで作成した Model を JSON とマッピングさせます。
    TextForm.java

    package com.example.app.form;
    
    import jakarta.validation.constraints.NotBlank;
    import lombok.Data;
    
    @Data
    public class TextForm {
        // ユーザが入力した値はここに格納
        @NotBlank(message = "文章を入力してください")
        String inputText;
    }
    

    ユーザが入力した文章を inputText というフィールドで保持します。
    ユーザが何も入力せずに送信ボタンを押下した場合の警告文を message で定義しています。

    • Model (Python からの JSON 形式のレスポンスをオブジェクトとして受け取る)

    ApiResponse.java

    package com.example.app.model;
    
    import com.fasterxml.jackson.annotation.JsonProperty;
    import lombok.Data;
    
    import java.util.List;
    
    @Data
    public class ApiResponse {
        // Python のレスポンスがリストなので Sentiment 型オブジェクトをリストで保持
        @JsonProperty("sentiment_list")
        List<Sentiment> sentiments;
    }
    

    Sentiment.java

    package com.example.app.model;
    
    import lombok.Data;
    
    @Data
    public class Sentiment {
        // Python が返す JSON の形式に合わせる
        // レスポンスの例:{'positive': 1, 'negative': 0}
        int positive;
    
        int negative;
    }
    
    • Service (Python から受け取ったデータの処理)

    Service では Pytnon から受け取ったデータの処理をします。
    文章全体のネガポジの判断はここで決定します。
    SentimentService.java

    package com.example.app.service;
    
    import com.example.app.model.ApiResponse;
    import com.example.app.model.Sentiment;
    
    public class SentimentService {
    
        public static String createResult(ApiResponse response) {
    
            int score = calcSentiment(response);
    
            if (score == 0) {
                return "neutral";
            }
    
            return score > 0 ? "positive" : "negative";
        }
    
        private static int calcSentiment(ApiResponse response) {
    
            // response の sentiments に格納されているポジティブ、ネガティブの値をそれぞれ合算
            int positiveScore = response.getSentiments().stream().mapToInt(Sentiment::getPositive).sum();
            int negativeScore = response.getSentiments().stream().mapToInt(Sentiment::getNegative).sum();
    
            final int total = positiveScore + negativeScore;
    
            // 合計が 0 の場合は式が成り立たないので 0 を返す。0でなければ結果を計算して返す
            return total == 0 ? 0 : (positiveScore - negativeScore) / total;
        }
    }
    

    実行用のクラス

    Spring Boot のアプリケーションを実行するためのクラスです。
    Application.java

    package com.example.app;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.openfeign.EnableFeignClients;
    
    @SpringBootApplication
    @EnableFeignClients // @FeignClient を使用するために必要
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    

    Python の実装

    続いて、ApiClient からのリクエストを受け取る Flask(Python) 側の実装です。
    バージョンは Python 3.10.4、Flask 3.1.0 を使用します。
    app.py

    from flask import Flask, request, jsonify
    import oseti
    
    app = Flask(__name__)
    
    @app.route('/sentiment') # localhost:5000/ にアクセスで実行
    def microAppFunc():
        # リクエストパラメータ text の値を取得
        text = request.args.get('text')
        # ネガポジ分析し、リスト形式で結果を返す
        # 例:[{'positive': 1, 'negative': 0}, {'positive': 0, 'negative': 0}]
        result = oseti.Analyzer().count_polarity(text.rstrip())
        # リストを JSON に整形
        return jsonify({'sentiment_list' : result})
    
    if __name__ == '__main__':
        app.run(host='localhost', port=5000) #ポート5000でサーバーを起動する
    

    今回は感情分析には oseti というライブラリを使用してします。
    参考にしたサイトを以下に記載しますのでぜひ参考にしてみてください。

    note.com

    Thymeleaf (画面の実装)

    今回は Bootstrap というフレームワークを使用して画面を作成しています。
    実際にユーザが入力する画面の実装は以下になります。
    index.html

    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>感情判定ツール</title>
        <link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
              integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" rel="stylesheet">
        <link rel="stylesheet" th:href="@{/style.css}">
    </head>
    <body>
    <div class="container">
        <h2 class="text-center mb-4">感情判定ツール</h2>
        <form action="/sentiment" method="get" th:object="${textForm}">
            <div class="mb-3">
                <label class="form-label">分析したい文章を入力してください</label>
                <textarea class="form-control" placeholder="ここに文章を入力してください..." rows="5"
                          th:field="*{inputText}"></textarea>
                <div style="color: red;" th:errors="*{inputText}" th:if="${#fields.hasErrors('inputText')}"></div>
            </div>
            <button class="btn btn-primary" type="submit">判定する</button>
        </form>
    </div>
    <!-- 結果の表示 -->
    <div class="container" th:if="${result}">
        <div>
            <div class="result-icon">
                <span th:if="${result} == 'positive'">
                    ポジティブな文章です😊</span>
                <span th:if="${result} == 'negative'">
                    ネガティブな文章です😢</span>
                <span th:if="${result} == 'neutral'">ニュートラルな文章です</span>
            </div>
        </div>
    </div>
    </body>
    </html>
    

    style.css

    body {
        background-color: lightgray;
        font-family: "Arial";
    }
    .container {
        margin-top: 50px;
        max-width: 600px;
        background: #ffffff;
        border-radius: 8px;
        box-shadow:0 4px 6px rgba(0, 0, 0, 0.1);
        padding: 20px;
    }
    
    .btn {
        width: 100%;
    }
    

    このコードで作成した画面に実際にアクセスと以下の画像のような表示になります。

    実行画面

    アプリケーションの実行

    実際にアプリケーションを実行してみます。
    Spring Boot のアプリケーションと Flask の app.py を実行することでアプリケーションが実行できます。
    実行して localhost:8080/ にアクセスし、実際に文章を入力してみましょう!

    この場合は「いい天気」と入力したので結果はポジティブになるはずです。
    入力後に「判定する」ボタンを押下すると結果が表示されます。

    判定されました!
    ちゃんとポジティブな文章と判定されますね!
    続いてネガティブな文章を入力してみたいと思います。

    さっそく判定していきましょう。

    「仕事疲れた」はさすがにネガティブですよね(笑)
    想定どおりの結果で安心しました。
    では最後に意味のない文章を入力したらどうなるでしょうか。
    感情が読み取れないと Flask から positive = 0 , negative = 0 が返ってくるのでニュートラルになるはずです。

    ほげほげふがふがと入力して判定ボタンを押してみます。

    やはり感情が読み取れずニュートラルな文章と判定されました。
    このように Spring Boot で構築したサービスの処理の一部を API 経由で Python で構築したサービスに渡すことで Spring Boot で自然言語処理のアプリケーションを構築することが可能になります。

    さいごに

    今回は Java(Spring Boot)と Python(Flask)を組み合わせて簡易的なマイクロサービスアーキテクチャを構築してみました。
    コントローラ側にルーティングを行うメソッドを追加し、コントローラからのリクエストを受け取るサービスモジュールを新たに追加することで簡単に機能の追加も可能です。 実際に実装してみて、ユーザからのリクエストの処理は Java で行い、他の言語(サービス)の方が得意な処理はそちらに任せるといった構成が取れるので柔軟な技術選定のしやすさを感じました。
    またなにか興味が湧いた技術があればブログに書こうかなと思います。