0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Javaの実行の仕組みと、Shell・Java間の機能呼び出しを整理する

0
Posted at

JavaでAPIやバッチを開発していると、

  • Javaはコンパイルすると何になるのか
  • Javaの機能はどうやって呼び出されるのか
  • ShellからJavaを呼び出すのはなぜか
  • Javaから直接Javaの機能を呼ぶこともできるのか
  • 新しく作るならShellとJavaのどちらがよいのか

といった疑問が出てくる。

これらは別々の話に見えるが、実際には、

プログラムをどう実行し、機能同士をどう接続するか

という一つの話としてつながっている。


1. そもそもJavaはどうやって実行される?

Javaでは、書いたソースコードをそのままCPUが実行しているわけではない。

Javaソースコード
Main.java
    ↓
javac
    ↓
Javaバイトコード
Main.class
    ↓
JVM
    ↓
インタプリタ / JITコンパイラ
    ↓
CPUが実行できる機械語

例えば、

public int add(int a, int b) {
    return a + b;
}

というJavaコードを書いたとする。

これをコンパイルすると、

Example.java
    ↓ javac
Example.class

となる。

.classファイルの中に入っているのがJavaバイトコードである。

Javaバイトコードは広い意味ではバイナリデータだが、

CPUが直接実行するネイティブ機械語ではない

という点が重要である。

JVM(Java Virtual Machine)がバイトコードを読み取り、実行する。


2. .jarとは?

Java開発では.jarというファイルもよく登場する。

大まかには、

複数の.classファイルや設定ファイルなどをまとめたもの

と考えると分かりやすい。

application.jar
│
├── Main.class
├── Service.class
├── Repository.class
└── 設定ファイルなど

Shellなどから、

java -jar application.jar

と実行できる。


3. ShellからJavaを呼び出す

バッチシステムなどでは、

ジョブ管理
    ↓
Shell
    ↓
Java
    ↓
業務処理
    ↓
DB / ファイル / 外部システム

という構成を取ることがある。

例えば、

java -jar batch.jar input.csv

のようにJavaプログラムを起動する。

Shellは、

Javaを起動
    ↓
引数を渡す
    ↓
終了を待つ
    ↓
終了コードを確認
    ↓
次の処理を決める

という処理の制御役を担当できる。


4. なぜShellを使うのか?

Shellには、

OS上のプログラムやファイルを簡単につなぎ合わせられる

という強みがある。

例えば、

Shell
 ├─ JavaバッチAを起動
 ├─ ファイル存在確認
 ├─ ファイルを移動
 ├─ JavaバッチBを起動
 └─ 終了コード確認

といった処理はShellが得意である。


5. JavaからJavaの機能を直接呼ぶこともできる

同じJavaアプリケーション内なら、普通のメソッド呼び出しとして実現できる。

Result result = functionA.execute();

if (result.isSuccess()) {
    functionB.execute(result);
}

例えばSpring Bootなら、

HTTP Request
    ↓
Controller
    ↓
Service A
    ↓
Service B
    ↓
Repository
    ↓
PostgreSQL

という構成も作れる。


6. Shell → Java と Java → Java の違い

Shell → Java

Shell
 ↓
Javaプログラム

Shellが別のプログラムを起動する

java -jar batchA.jar

Java → Java

同じJVM・同じアプリケーションなら、

Java Application
 ├─ Service A
 │      ↓
 └─ Service B

として、

serviceB.execute(data);

のように直接呼び出せる。


7. ShellとJavaのメリット・デメリット

観点 Shell Java
単純な処理の手軽さ
プログラム起動
OS操作
ファイル操作
複雑な業務ロジック
型安全性
エラー処理
単体テスト
IDE支援
大規模化した場合の保守

重要なのは、

ShellとJavaのどちらが優れているかではなく、何を担当させるか

である。


8. 一から作るならShellのほうが手軽なのか?

単純なプログラム起動ならShellはかなり手軽である。

java -jar functionA.jar

if [ $? -eq 0 ]; then
    java -jar functionB.jar
fi

一方、業務処理そのものを連携させるなら、

Result result = functionA.execute();

if (result.isSuccess()) {
    functionB.execute(result);
}

のようにJavaで表現したほうが自然な場合が多い。


9. 現在の開発では手軽さと可読性のどちらを取る?

基本的には、

最初に書くときの手軽さだけでなく、長期的な可読性・保守性を重視する

と考える。

ただし、

可読性重視
 ↓
何でもJava

という意味ではない。

単純な起動だけなら、

java -jar batch.jar

のほうがむしろ分かりやすい。

重要なのは、

その処理を最も単純に表現できる技術を使うこと

である。


10. プロセス・JVM・モジュール・サービスの境界は何が違うのか?

ここまで理解すると、次に重要になるのが**「境界」**である。

Javaシステムを見ていると、

  • モジュール
  • JVM
  • プロセス
  • サービス

という言葉が出てくる。

これらはすべて「何かを分ける」という意味を持っているが、分けているものが違う。

まず全体像を単純化すると、

OS
│
├─ Process A
│    │
│    └─ JVM
│         │
│         ├─ Module A
│         ├─ Module B
│         └─ Module C
│
└─ Process B
     │
     └─ JVM
          │
          ├─ Module D
          └─ Module E

さらに、これらのプロセスを独立したシステム機能として運用すると、

Service A
   │
   └─ Process
        └─ JVM
             ├─ Module A
             └─ Module B

             HTTP / Message

Service B
   │
   └─ Process
        └─ JVM
             └─ Module C

のような構成になることがある。


11. モジュールの境界

モジュールは、

コードや責務を整理するための境界

である。

例えば、

Application
│
├─ Contract Module
│
├─ Billing Module
│
└─ File Module

と分割する。

しかし、これらは同じJavaアプリケーションの中に存在できる。

billingService.calculate();

のように直接呼び出すことも可能である。

つまり、

Module A
   ↓ メソッド呼び出し
Module B

となる。

この場合、

  • 同じプロセス
  • 同じJVM
  • 同じメモリ空間

で動いている可能性がある。

したがって、

モジュールを分けたからといって、プログラムそのものが別々に動いているとは限らない。

ここは非常に重要である。


12. JVMの境界

JVMは、

Javaバイトコードを実行するための実行環境

である。

例えば、

java -jar app.jar

と実行すると、通常そのJavaアプリケーションを動かすJVMが起動する。

イメージとしては、

JVM
│
├─ ContractService
├─ BillingService
├─ FileService
│
├─ Heap
├─ Threads
└─ Garbage Collector

となる。

同じJVMの中にいるJavaオブジェクト同士なら、

billingService.execute(data);

のような通常のJava呼び出しができる。

しかしJVMが分かれると、

JVM A

ContractService
      │
      │ 直接メソッド呼び出しはできない
      ↓
JVM B

BillingService

となる。

別JVMのJavaオブジェクトを、そのまま普通のJavaメソッドとして呼び出すことはできない。

そこで、

HTTP
RPC
Message Queue
Socket

などの通信方法が必要になる。


13. プロセスの境界

プロセスとは、

OSが管理する実行中のプログラムの単位

である。

例えば、

java -jar appA.jar
java -jar appB.jar

を別々に起動すると、通常は別々のJavaプロセスとして動く。

OS
│
├─ Process A
│    └─ JVM A
│         └─ appA
│
└─ Process B
     └─ JVM B
          └─ appB

プロセスが違えば、基本的にメモリ空間も分離される。

そのため、

Process A
    ↓
   ×
    ↓
Process B

と直接オブジェクトを渡すことはできない。

代わりに、

Process A
    ↓
HTTP / RPC / File / Message
    ↓
Process B

などを使う。

Shellから、

java -jar batchA.jar
java -jar batchB.jar

と起動する場合も、

Shell
 ├─ Java Process A
 └─ Java Process B

という境界が生まれる。


14. JVMとプロセスは何が違う?

ここは少し紛らわしい。

Javaアプリケーションでは多くの場合、

Java Process
     │
     └─ JVM

となるため、

「JVMが違う」

「Javaプロセスが違う」

がほぼ同時に起きる。

ただし概念としては、

Process
=OSが管理する実行単位

JVM
=Javaを実行する仮想マシン

であり、意味は違う。


15. サービスの境界

サービスは少し性質が違う。

サービスとは、

ある業務・機能上の責務を外部に提供する単位

として考えると分かりやすい。

例えば、

注文サービス
決済サービス
在庫サービス

という分割がある。

それぞれを独立したアプリケーションとして動かせば、

Order Service
     ↓ HTTP
Payment Service
     ↓ HTTP
Inventory Service

のようになる。

マイクロサービスでは特に、

Service A
・独立した責務
・独立したデプロイ

Service B
・独立した責務
・独立したデプロイ

という独立性が重要になる。


16. サービス = プロセスではない

ここも重要である。

「サービス」という言葉は論理的・アーキテクチャ的な概念であり、

「プロセス」はOS上の実行単位である。

そのため、

サービス
≠
必ず1プロセス

である。

例えば負荷が高くなれば、

Order Service

     ┌→ Process A
Request
     ├→ Process B
     │
     └→ Process C

のように、同じサービスを複数プロセス・複数インスタンスで動かすこともある。

Kubernetesなどで、

Order Service
│
├─ Pod A
├─ Pod B
└─ Pod C

とスケールさせるイメージにもつながる。

したがって、

サービスは「何を提供するか」の境界、プロセスは「どう実行されているか」の境界

と考えると区別しやすい。


17. 4つを比較する

境界 何を分ける? 分かれると何が起きる?
モジュール コード・責務 コード上の依存関係を整理できる
JVM Java実行環境 Javaオブジェクトを直接共有できなくなる
プロセス OS上の実行単位 メモリ・実行状態などが分離される
サービス システム・業務上の責務 API等を契約として独立して連携できる

つまり、分離のレベルが全部同じではない。


18. 具体例で考える

例えば、

契約
請求
ファイル出力

という3機能があるとする。

パターンA:同じJavaアプリケーション

1 Process
└─ 1 JVM
    │
    ├─ Contract Module
    ├─ Billing Module
    └─ File Module

呼び出しは、

billingService.execute();

のようにできる。

これはコード上では分割されているが、実行環境としては一緒である。

モジュラーモノリスなどでは、このような構造が考えられる。


パターンB:別Javaプログラムとして実行

Shell
│
├─ Process A
│    └─ JVM
│         └─ Contract
│
├─ Process B
│    └─ JVM
│         └─ Billing
│
└─ Process C
     └─ JVM
          └─ File

この場合、Javaプログラム同士は直接メソッド呼び出しできない。

Shell、ファイル、DBなどを介して処理をつなぐことになる。


パターンC:サービスとして分離

Contract Service
      │
      │ HTTP / Message
      ↓
Billing Service
      │
      │ HTTP / Message
      ↓
File Service

各サービスを独立して起動・デプロイできる。

ただし、その代わり、

  • ネットワーク通信
  • タイムアウト
  • リトライ
  • APIのバージョニング
  • 障害時の扱い
  • 分散トレーシング

など、分散システム特有の問題も考える必要が出てくる。


19. 境界を強くすると独立性は上がるが、コストも上がる

ここがアーキテクチャを考えるうえでかなり重要である。

同じJVMなら、

A
↓
B

というメソッド呼び出しだけで済む。

非常に速く、単純である。

一方、サービスを分けると、

Service A
 ↓
JSONへ変換
 ↓
HTTP
 ↓
Network
 ↓
Service B
 ↓
JSONをJavaオブジェクトへ変換

のような処理が必要になる。

さらにService Bが落ちている可能性もある。

つまり、

境界が弱い
↓
連携が簡単
↓
独立性が低い


境界が強い
↓
独立性が高い
↓
連携が複雑

というトレードオフがある。

そのため、

「分割できるから分割する」のではなく、「分割するメリットが複雑さを上回るか」を考える

必要がある。


20. Shellの話ともつながる

最初の疑問だった、

ShellからJavaを呼ぶのと、JavaからJavaを呼ぶのは何が違う?

も、境界として見ると理解しやすい。

Java
 ↓ メソッド
Java

=同じJVM内で完結できる

一方、

Shell
 ↓
java -jar A.jar

Shell
 ↓
java -jar B.jar

なら、通常は、

Process A / JVM A

Process B / JVM B

というプロセス・JVMの境界ができる。

つまりShellを挟むことは、単なる「書き方の違い」ではなく、場合によっては実行単位そのものを分離する設計にもつながる。


21. 最終的に見るべきポイント

システムを見るとき、

「機能ごとに分かれています」

と言われただけでは、実は情報が足りない。

次のように考える必要がある。

機能は分かれている?
        ↓
コードのModuleだけ?

それとも
        ↓
別JVM?

それとも
        ↓
別Process?

それとも
        ↓
独立Service?

例えば、

機能A
機能B
機能C

と分かれていても、

1 JVM
├─ A
├─ B
└─ C

なのか、

JVM A
JVM B
JVM C

なのかでは、アーキテクチャとして全く意味が違う。


22. まとめ

Javaシステムを見るときは、次の4つを区別すると理解しやすい。

Module
    ↓
コード・責務の境界

JVM
    ↓
Java実行環境の境界

Process
    ↓
OS上の実行単位の境界

Service
    ↓
システム・業務責務の境界

そして、

1 Process
└─ JVM
   ├─ Module A
   ├─ Module B
   └─ Module C

という構成もあれば、

Service A
└─ Process / JVM

        ↓ HTTP

Service B
└─ Process / JVM

という構成もある。

重要なのは、

「機能が分かれているか」だけではなく、「どの境界で分かれているのか」を確認すること。

これが分かると、

  • モノリスなのか
  • モジュラーモノリスなのか
  • 複数のバッチプログラムなのか
  • サービスとして分割されているのか
  • マイクロサービスなのか

を判断するための材料が増える。

そして設計するときも、

どこまでを同じJVMに置き、どこでプロセスを分け、どこを独立したサービスとして扱うのか

という形で考えられるようになる。

これはまさに、アーキテクチャにおける「境界を設計する」という考え方につながっている。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?