谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

Spring Boot + MOA その2 - Service Interfaceで呼び出しを明確にするよ?

前回のエントリーでは、Spring Bootで@RestControllerアノテーションを使ったサーバを作り、
RestTemplateでクライアントを作ることで、リモートサービス呼び出しを実装しました。
http://d.hatena.ne.jp/cero-t/20150618/1434645164


しかしこの方法は、クライアント/サーバ間に何の契約もなく、
サーバから呼び出し元のクライアントをたどることもできません、
言ってしまえば、リフレクションによるメソッド呼び出しをしているようなものでした。

リモートサービス呼び出しにインタフェースを導入する

この問題を解決するために、クライアント/サーバ間の契約として
インタフェースを挟むことを検討します。


サンプルは、以下に置いています。
https://github.com/cero-t/moa-sample/tree/master/moa-sample-2


それでは実装を順番に考えていきます。
まずは共通の「domain」モジュールに、Serviceのinterfaceを作成します。
https://github.com/cero-t/moa-sample/blob/master/moa-sample-2/student-domain-2/src/main/java/service/StudentService.java

public interface StudentService {
    Student get();

    int post(Student student);
}

これがリモートサービスで公開するメソッドの一覧になります。


続いて「service」モジュールのControllerを、このinterfaceの実装とします。

@RestController
public class StudentController implements StudentService {
    @RequestMapping(value = "/student", method = RequestMethod.GET)
    @Override
    public Student get() {
        Student student = new Student();
        student.id = 1;
        student.name = "TEST";
        student.score = 100;
        return student;
    }

    @RequestMapping(value = "/student", method = RequestMethod.POST)
    @Override
    public int post(Student student) {
        return 1;
    }
}

実装の内容は前回と変わらず、クラスをimplements StudentServiceとしただけです。


さらにサービスを呼び出すためのクラスも、同じくこのinterfaceの実装として作成します。
https://github.com/cero-t/moa-sample/blob/master/moa-sample-2/student-client-2/src/main/java/StudentServiceRemote.java

public class StudentServiceRemote implements StudentService {
    @Override
    public Student get() {
        ResponseEntity<Student> result = new RestTemplate().getForEntity("http://localhost:8080/student", Student.class);
        return result.getBody();
    }

    @Override
    public int post(Student student) {
        return new RestTemplate().postForObject("http://localhost:8080/student", student, Integer.class);
    }
}

前回はクライアント側のソースコード内にあったサービス呼び出しの処理を、ここに集約します。


このサービス呼び出し用クラスは、
「domain」モジュールに置いても「client」モジュールに置いても良いと思います。


リモートサービス呼び出しはクライアント側の責務だと考えれば「client」に置くことが正しいでしょうし、
「domain」に置くことで、リモートサービス呼び出しの共通ユーティリティとして利用できるので便利です。
今回、僕は「client」モジュールに置くことにしました。


最後に、このサービス呼び出しクラスを、クライアント側で利用します。
https://github.com/cero-t/moa-sample/blob/master/moa-sample-2/student-client-2/src/main/java/StudentClient.java

public class StudentClient {
    protected StudentService studentService = new StudentServiceRemote();

    public static void main(String[] args) {
        new StudentClient().execute();
    }

    public void execute() {
        Student student = studentService.get();
        System.out.println(student.id);
        System.out.println(student.name);
        System.out.println(student.score);

        Integer value = studentService.post(student);
        System.out.println(value);
    }
}

newしている辺りが若干微妙ですが、どうあれこれで、
クライアント/サーバ間で、同じインタフェースを共有することできました。


この方法を用いれば、Controller側のメソッドからOpen Call Hierarchyを行うことで
クライアント側のメソッド呼び出しをたどることができるようになり、形も綺麗になりました。

クライアント側もSpring Bootで。

おまけみたいなものですが、クライアント側もSpringベースにするため、少しだけ修正します。
サンプルは以下です。
https://github.com/cero-t/moa-sample/tree/master/moa-sample-3



リモートサービス呼び出しのクラスをSpringが管理するよう、@Componentアノテーションを追加します。
https://github.com/cero-t/moa-sample/blob/master/moa-sample-3/student-client-3/src/main/java/application/remote/StudentServiceRemote.java

@Component
public class StudentServiceRemote implements StudentService {
    @Override
    public Student get() {
        ResponseEntity<Student> result = new RestTemplate().getForEntity("http://localhost:8080/student", Student.class);
        return result.getBody();
    }

    @Override
    public int post(Student student) {
        return new RestTemplate().postForObject("http://localhost:8080/student", student, Integer.class);
    }
}

相変わらず実装は変わりません。URLの直書き感もそのままです。


そしてクライアント側では、上で作ったクラスがDIされるよう@Autowiredアノテーションを追加します。
https://github.com/cero-t/moa-sample/blob/master/moa-sample-3/student-client-3/src/main/java/application/client/StudentClient.java

public class StudentClient {
    @Autowired
    StudentService studentService;

    public void execute() {
        Student student = studentService.get();
        System.out.println(student.id);
        System.out.println(student.name);
        System.out.println(student.score);

        Integer value = studentService.post(student);
        System.out.println(value);
    }
}

これでリモートサービス呼び出し部分が、綺麗に隠れました。

ここまでのまとめ

今回、クライアント/サーバ間の契約として、Serviceのinterfaceを利用しました。
このような形にしておけば、サーバとクライアントの呼び出し関係が明確になり、
IDEのOpen Call Hierarchy機能を利用して、呼び出しをたどることもできるようになります。


また(別に、interfaceを導入したためというわけではないですが)
JUnitでのテスト時に、リモートサービス呼び出し部分をモック化しやすくなりました。


これがリモートサービス呼び出しの最もベーシックな形になると思います。
それなりに大きな規模の案件で、たとえ十分に管理ができなくなってきたとしても、
この形さえ守られていれば、後からサーバとクライアントの呼び出し関係を整理することができます。


ただ、サービス呼び出し用のServiceRemoteクラスを毎度自前で実装するのは面倒です。
次回はこの辺りの改善を考えてみます。