革命のブログ

フレボワークスの社員がブログを通じて情報発信します。

マイクロモノリスという新しいアーキテクチャ

どうも小野です。

昨今、アーキテクチャ界隈では、マイクロサービス化が主流になってきているようです。 マイクロサービスの管理コストを抑えるために、Kubernetesなどのコンテナオーケストレーションツールの利用が必要になってきています。

もちろん、導入できれば抑えることができるかもしれませんが、導入に至るまでの学習コストがかかります。

「そもそも、そのシステムにマイクロサービス化は必要ですか?」

マイクロサービスを推進する記事には、モノリシックなシステムをディスる内容が多いですが、 規模によってはモノリシックのほうがシステム要件にマッチすることもありますし、それが殆どだと思っています。

流行っている、モノリシックよりメリットが大きいなど自分で調査せずに導入すると、 首を締めることになりかねません。

まずは、本当に必要かどうかチームメンバーと話し合ってください。

今回は、私個人が考えてみたアーキテクチャについてご紹介します。

マイクロサービスのメリット、デメリット

マイクロサービスのメリットとデメリットを私なりに挙げてみます。

メリット

  • 各サービスごとに言語、DBが選べる。
  • サービスごとの規模が小さいためコードの見通しがよい。改修時の影響範囲を把握しやすい。
  • あるサービスがダウンしても他のサービスに影響しない。そのサービスを参照しているところは影響あり。
  • サービスごとにリリースが可能なため、サイクルが早い。
  • サービスごとにスケーリングが可能

デメリット

  • RDBの良さを活かせない。(マイクロサービスにするくらいなら、RDBは使わないほうがいいと思ってる)
  • サービス間通信のオーバーヘッド
  • サービス間の分散トランザクション処理の複雑化(マイクロサービスでは、サービス間のデータ不整合を許容する設計にすることが多い)
  • 障害時の原因特定がモノリシックに比べ大変(ログ次第)
  • オーケストレーションツールなしでは管理は難しい
  • N+1問題

私はマイクロサービスの導入するにあたり、次の問題にぶつかります。

なぜ、マイクロサービスでは、データベースを分割しなければいけないのか?

もし、サービスごとにデータベース分割をする必要がないのであれば、 私は新たなアーキテクチャを提案できます。

それは、タイトルにもある通り「マイクロモノリス」というアーキテクチャです。

その名の通り、マイクロサービスとモノリシックをかけ合わせたもので、 よく言えばいいとこ取りです。

マイクロモノリスのメリット、デメリット

マイクロモノリスを使うことによるメリット、デメリットを先程のマイクロサービスと照らし合わせて挙げてみます。

メリット

  • RDBのメリットが活かせる
  • サービス間通信がないので、ネットワークによるオーバーヘッドがない
  • サービス間でのトランザクションがないため、処理が簡素化
  • サービスごとの規模が小さいためコードの見通しがよい。改修時の影響範囲を把握しやすい。
  • あるサービスがダウンしても他のサービスに影響しない。
  • サービスごとにリリースが可能なため、サイクルが早い。
  • サービスごとにスケーリングが可能
  • 関連サービスのデータは結合(Join)して取得するので、N+1問題は起きない。

デメリット

  • 同じデータベースを参照するので、負荷が上がる。
  • サービスごとに言語、DBが選べない。

マイクロサービスと同様に小さい単位でサービスが分かれる一方で中身の実装はモノリスになります。

では、どのように実現するか、実際にコードを載せながら説明していきます。

実現方法

技術スタック

  • Java
  • SpringBoot

※他の言語、FWをお使いの方は、置き換えながら読んでください。

プロジェクト構成

作成するプロジェクトは1サービスあたり、メインのプロジェクト、ドメイン部分を抽象化したプロジェクトの2つを作成します。 今回、サンプルとしてCompanyサービスとUserサービスを作成しますので、全部で4つになります。

Companyサービスのプロジェクトは以下のようになります。

Companyサービスのメインプロジェクト

f:id:frevo-works:20190704143420p:plain
メインプロジェクトのパッケージ構成

Companyサービスのドメインプロジェクト

f:id:frevo-works:20190704143443p:plain
ドメインプロジェクトのパッケージ構成

メインプロジェクトからドメインプロジェクトを参照します。

<dependency>
    <groupId>com.micromonolith</groupId>
    <artifactId>company-domain</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <type>jar</type>
</dependency>

Userサービスも同様の構成で作成します。

ドメインプロジェクト

Model

package com.micromonolith.domain.model;

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

@MappedSuperclass
public abstract class AbstractCompany {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long id;

    @Column
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

フィールドは、DBのテーブルと完全に一致させてください。 単独でインタンス化させない親の抽象クラスとして定義します。

Repository

package com.micromonolith.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ICompanyRepository<T,R> extends JpaRepository<T, R>{
}

各サービスで利用頻度が多そうなインターフェースを定義します。 今回は簡単なサンプルのため、JpaRepositoryのみ継承しています。

Service

package com.micromonolith.domain.service;

import java.util.List;

import com.micromonolith.domain.repository.ICompanyRepository;

public abstract class AbstractCompanyService<T,R> {
    
    protected ICompanyRepository<T,R> companyRepository;
    
    public T find(R id) {
        return companyRepository.findById(id).orElse(null);
    }
    
    public List<T> findAll(){
        return companyRepository.findAll();
    }
    
    public T save(T entity) {
        return companyRepository.save(entity);
    }
}

抽象クラスとして定義します。

メソッドには、利用頻度が多い機能のみ定義しておきます。

ドメインの抽象化プロジェクトは以上です。

メインプロジェクト

では、メインプロジェクトの方を見ていきます。

Model

package com.micromonolith.company.domain.model;

import java.util.List;

import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import com.micromonolith.domain.model.AbstractCompany;

@Entity
@Table(name = "companies")
public class Company extends AbstractCompany {
    @OneToMany
    @JoinColumn(name = "company_id")
    private List<User> users;

    public List<User> getUsers() {
        return users;
    }

    public void setUsers(List<User> users) {
        this.users = users;
    }
}

ドメインプロジェクトで作成したAbstractCompanyを継承します。継承するクラスは必ず、「Abstract+作成するクラス名」の名前のクラスを継承してください。(ルール的に)あと、今回のサンプルでは、Companyに所属するUserを取得したいので、usersを定義しています。

Userクラスは以下の通り。

package com.micromonolith.company.domain.model;

import javax.persistence.Entity;
import javax.persistence.Table;

import com.micromonolith.domain.model.AbstractUser;

@Entity
@Table(name = "users")
public class User extends AbstractUser {
}

AbstractUserはUserのドメインプロジェクトで作成したものです。 Companyサービスとしては、Userを主にしたデータ取得はないので、追加するフィールドはありません。

Repository

package com.micromonolith.company.domain.repository;

import org.springframework.stereotype.Repository;

import com.micromonolith.company.domain.model.Company;
import com.micromonolith.domain.repository.ICompanyRepository;

@Repository
public interface CompanyRepository extends ICompanyRepository<Company, Long> {
    // 他に追加したい機能があれば追加する
}

ドメインプロジェクトで作成したICompanyRepositoryを継承します。継承する際、ジェネリック型も指定します。

Service

package com.micromonolith.company.domain.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.micromonolith.company.domain.model.Company;
import com.micromonolith.company.domain.repository.CompanyRepository;
import com.micromonolith.domain.service.AbstractCompanyService;

@Service
public class CompanyService extends AbstractCompanyService<Company, Long> {

    @Autowired
    public void setCompanyRepository(CompanyRepository companyRepository) {
        this.companyRepository = companyRepository;
    }

}

ドメインプロジェクトで作成したAbstractCompanyServiceを継承します。継承する際、ジェネリック型も指定します。 あと、親クラスでrepositoryのメンバ変数を持っているので、継承クラスでは、setterを用意します。

コントローラはいつもどおりに実装で問題ないので、割愛します。 一応、ソースをGitHubに上げておくので、興味がある人は見てください。

マイクロモノリスの実装ルールをまとめます。

  • 他サービスで共通部品として使うドメインロジックは、対象のサービスのドメインプロジェクトに実装する
  • 他サービスのドメインロジックを独自に拡張したい場合は、各メインプロジェクトに実装する

図で表すとこんなイメージです。

f:id:frevo-works:20190704175446p:plain
マイクロモノリスのイメージ

実装パターン

マイクロモノリスの実装ルールが明確になったので、いくつかの実装パターンを見ていきましょう。

要件

Companyマスタに住所を追加したい。

実装方法

CompanyサービスのドメインプロジェクトのAbstractCompany.javaを修正する。

@MappedSuperclass
public abstract class AbstractCompany {

    〜省略〜

        // 以下を追加
        private String address;

        public String getAddress(){
                return address;
        }

        public void setAddress(String address){
                this.address = address;
        }
}

要件

企業情報を取得する際に、所属するユーザの氏名の後ろに「様」をつけたものを取得したい。

実装方法

CompanyプロジェクトのUser.javaにgetterを追加する。

public class User extends AbstractUser {
        // 追加
        public String getName(){
                return  name + "様";
        }
}

もちろん、Userサービスのユーザ名に影響なし。

要件

ユーザ名で検索するメソッドを追加し、Companyサービスでも利用したい。

実装方法

IUserRepository、AbstractUserServiceにユーザ名で検索するメソッドを追加。

CompanyプロジェクトにUserRepositoryとUserServiceを追加。

public interface IUserRepository<T, R> extends JpaRepository<T, R> {
         // 追加
    public List<T> findByName(String name);
}
public abstract class AbstractUserService<T, R> {
    〜省略〜

         // 追加
    public List<T> findByName(String name) {
        return userRepository.findByName(name);
    }
}

例えば、3つ目の要件をマイクロサービスで行う場合の次の手段をとるかと思います。

  1. Userサービスには、リクエストを受け付けるためのAPIを用意する必要があります。

  2. Companyサービスから、Userサービスにクエリパラメータに氏名をつけてリクエストします。

マイクロモノリスとマイクロサービスの実装方法の違いについて、理解できましたか?

マイクロモノリスは、他のサービスのドメインロジックを抱えることになりますが、ドメイン層で完結しますのでロジック的にも簡潔になると思います。

さいごに

マイクロモノリスのイメージは湧きましたか?

ある人は言ってました。

「マイクロサービスを実現するために、ネットや書籍などで書いてあることを忠実に再現することが本質ではない。」

要は、マイクロサービスの正解はありません。 マイクロモノリスだって1つのマイクロサービスの形なのではないでしょうか。

一番大事なのは、システム、メンバーにとって一番良いアーキテクチャを選択することです。

マイクロサービスにしたいけど、複雑にしたくないとい方は、ぜひともマイクロモノリスも検討してみてください。

github.com