3層アーキテクチャとレイヤーの疎結合

📢 この記事は gemini-2.5-flash によって翻訳されました

事例導入

従業員データを取得して、統一された応答結果を返して、ページに表示するよ。

まず、XMLファイルを解析するためにdom4jの依存関係を追加する必要があるんだ。

1
2
3
4
5
<dependency>
    <groupId>org.dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>2.1.3</version>
</dependency>

XML解析用のユーティリティクラスXMLParserUtils、対応するエンティティクラスEmp、XMLファイルemp.xmlを導入するよ。

静的ページファイルをresources/staticに配置するね。

SpringBootプロジェクトの静的リソース(HTML5+CSS+JSなどのフロントエンドリソース)は、デフォルトでclsspath:/staticclasspath:/publicclsspath:/resourcesに置かれるんだ。

Mavenの場合、classpathはsrc/main/resourcesになるよ。

Controllerプログラムを書いて、リクエストを処理して、データを応答するんだ(この例ではコードは省略、後述の3層アーキテクチャを使うよ)。

3層アーキテクチャ

上の例のempコードだと、データアクセス、処理ロジック、リクエスト受信と応答が全部一つのControllerに入っちゃってるから、再利用性が低くて保守も難しいんだ。だから、単一責任の原則を満たすためにこれらを分離する必要がある。3層アーキテクチャにすると、コードの再利用性が高まって、保守しやすくなるし、拡張も楽になるよ。

3層アーキテクチャはController、Service、Daoに分かれるよ。

  • Controller:制御層で、フロントエンドから送られてきたリクエストを受け取って、処理してからデータを返すんだ。
  • Service:ビジネスロジック層で、具体的なビジネスロジックを処理するよ。
  • Dao:データアクセス層(Data Access Object)または永続化層で、データアクセス操作、つまりCRUDを担当するんだ。

ブラウザがリクエストを出す -> Controllerがリクエストを受け取ってデータを応答 -> Serviceがロジックを処理 -> Daoがデータアクセス

上の例のempコードは次のように最適化できるよ。

  1. Controller
1
2
3
4
5
6
7
8
9
@RestController
public class EmpController {
    @RequestMapping("/listEmp")
    public Result listEmp(){
        EmpService empServiceA = new EmpServiceA();
        List<Emp> empList = empServiceA.listEmp();
        return Result.success(empList);
    }
}
  1. Service
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// interface
public interface EmpService {
    public List<Emp> listEmp();
}

public class EmpServiceA implements EmpService {
    private EmpDao empDao = new EmpDaoA();
    public List<Emp> listEmp(){
        List<Emp> empList = empDao.listEmp();
        empList.forEach(emp -> {
            String gender = emp.getGender();
            if("1".equals(gender)){
                emp.setGender("男");
            } else if ("2".equals(gender)) {
                emp.setGender("女");
            }
            String job = emp.getJob();
            if("1".equals(job)){
                emp.setJob("講師");
            } else if ("2".equals(job)) {
                emp.setJob("班主任");
            } else if ("3".equals(job)) {
                emp.setJob("就業指導");
            }
        });
        return empList;
    }
}
  1. Dao
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// interface
public interface EmpDao {
    public List<Emp> listEmp();
}

public class EmpDaoA implements EmpDao {
    public List<Emp> listEmp(){
        String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
        List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
        return empList;
    }
}

レイヤーの疎結合

凝集度:ソフトウェアの各機能モジュール内部における機能的な関連性のこと。

結合度:ソフトウェアの各層/モジュール間の依存関係や関連性の度合いを測るもの。

ソフトウェア設計原則:高凝集度、低結合度

詳細は https://blog.yexca.net/archives/145 を見てね。

例えば、上の3層アーキテクチャの例だと、ControllerとServiceが結合してて、ServiceとDaoも結合してるよね。

Controllerで直接ServiceオブジェクトEmpService empServiceA = new EmpServiceA();みたいにAを使っちゃうと、Bに変更するときはControllerも変更しなきゃいけないんだ。Serviceを変えてもControllerを変更しないようにするためには、コンテナを作成して、Controllerがコンテナからオブジェクトを取得するように(依存性注入)、そしてServiceがサービスをコンテナに注入するように(制御の反転)できるよ。

  • 制御の反転:Inverse Of Control、略してIOC。オブジェクトの生成制御がプログラム自身から外部(コンテナ)に移されること。
  • 依存性注入:Dependency Injection、略してDI。コンテナがアプリケーションの実行時に、必要となるリソースを提供するんだ。
  • Beanオブジェクト:IOCコンテナで作成、管理されるオブジェクトのこと。

IOC

あるオブジェクトをIOCコンテナに管理してもらうには、対応するクラスに以下のいずれかのアノテーションを付ける必要があるよ。

アノテーション説明位置
@ComponentBeanを宣言する基本アノテーション以下の3つのカテゴリに属さない場合に使用する(ユーティリティクラスなど)
@Controller@Componentの派生アノテーションコントローラクラスに付ける
@Service@Componentの派生アノテーションビジネスロジッククラスに付ける
@Repository@Componentの派生アノテーションデータアクセス層のクラスに付ける(MyBatis連携ではあまり使わない)
  • Beanを宣言するときは、value属性で名前を指定できるよ。指定しない場合は、デフォルトでクラス名の先頭が小文字になるんだ。
  • SpringBootでのWeb開発では、コントローラBeanの宣言には@Controllerしか使えないよ。

Beanコンポーネントスキャン

上で宣言したBeanの4つのアノテーションは、有効にするにはコンポーネントスキャンアノテーション@ComponentScanでスキャンされる必要があるんだ。

このアノテーションは、実は起動クラスのアノテーション@SpringBootApplicationにすでに含まれてるんだ。デフォルトのスキャン範囲は、起動クラスのあるパッケージとそのサブパッケージになるよ。

valueまたはbasePackage属性でスキャン範囲を指定できるよ。

1
@ComponentScan({"dao","net.yexca"})

上の例では、

  1. Controller

@RestControllerアノテーションはすでに@Controllerを含んでいるから、修正は不要だよ。

  1. Service
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// interface
public interface EmpService {
    public List<Emp> listEmp();
}

// 実装クラスに追加
@Service // このクラスをIOCコンテナに管理させ、IOCコンテナのBeanにする
public class EmpServiceA implements EmpService {
    private EmpDao empDao = new EmpDaoA();
    public List<Emp> listEmp(){
        List<Emp> empList = empDao.listEmp();
        empList.forEach(emp -> {
            String gender = emp.getGender();
            if("1".equals(gender)){
                emp.setGender("男");
            } else if ("2".equals(gender)) {
                emp.setGender("女");
            }
            String job = emp.getJob();
            if("1".equals(job)){
                emp.setJob("講師");
            } else if ("2".equals(job)) {
                emp.setJob("班主任");
            } else if ("3".equals(job)) {
                emp.setJob("就業指導");
            }
        });
        return empList;
    }
}
  1. Dao
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// interface
public interface EmpDao {
    public List<Emp> listEmp();
}

// 実装クラスに追加
@Repository // このクラスをIOCコンテナに管理させ、IOCコンテナのBeanにする
public class EmpDaoA implements EmpDao {
    public List<Emp> listEmp(){
        String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
        List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
        return empList;
    }
}

DI

@Autowiredアノテーションを使うと依存性を注入できるんだけど、デフォルトではに基づいて行われるから、同じ型のBeanが複数あるとエラーになるんだ。

上記の例にDIを追加するよ(変更されたコードはコメントアウトするね)。

  1. Controller
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@RestController
public class EmpController {
    @Autowired // 実行時、IOCコンテナがこの型のBeanオブジェクトを提供し、この変数に代入するんだ -- 依存性注入
    private EmpService empService;
    
    @RequestMapping("/listEmp")
    public Result listEmp(){
        
        //EmpService empServiceA = new EmpServiceA();
        //List<Emp> empList = empServiceA.listEmp();
        List<Emp> empList = empService.listEmp();
            
        return Result.success(empList);
    }
}
  1. Service
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// interface
public interface EmpService {
    public List<Emp> listEmp();
}

// 実装クラスに追加
@Service // このクラスをIOCコンテナに管理させ、IOCコンテナのBeanにする
public class EmpServiceA implements EmpService {
    
    @Autowired // 実行時、IOCコンテナがこの型のBeanオブジェクトを提供し、この変数に代入するんだ -- 依存性注入
    private EmpDao empDao;
    // private EmpDao empDao = new EmpDaoA();
    
    public List<Emp> listEmp(){
        List<Emp> empList = empDao.listEmp();
        empList.forEach(emp -> {
            String gender = emp.getGender();
            if("1".equals(gender)){
                emp.setGender("男");
            } else if ("2".equals(gender)) {
                emp.setGender("女");
            }
            String job = emp.getJob();
            if("1".equals(job)){
                emp.setJob("講師");
            } else if ("2".equals(job)) {
                emp.setJob("班主任");
            } else if ("3".equals(job)) {
                emp.setJob("就業指導");
            }
        });
        return empList;
    }
}
  1. Dao

データ注入がないから、修正は不要だよ。

@Primary

もしServiceにEmpServiceBという別の実装クラスを追加して、それにも@Autowiredを使ったら、プログラムはエラーになるんだ。その場合は@Primaryアノテーションを使って、どれを使うか指定できるよ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// interface
public interface EmpService {
    public List<Emp> listEmp();
}

// 実装クラスに追加
@Service // このクラスをIOCコンテナに管理させ、IOCコンテナのBeanにする
public class EmpServiceA implements EmpService {
    
    @Autowired // 実行時、IOCコンテナがこの型のBeanオブジェクトを提供し、この変数に代入するんだ -- 依存性注入
    private EmpDao empDao;
    // private EmpDao empDao = new EmpDaoA();
    
    public List<Emp> listEmp(){
        List<Emp> empList = empDao.listEmp();
        empList.forEach(emp -> {
            String gender = emp.getGender();
            if("1".equals(gender)){
                emp.setGender("男");
            } else if ("2".equals(gender)) {
                emp.setGender("女");
            }
            String job = emp.getJob();
            if("1".equals(job)){
                emp.setJob("講師");
            } else if ("2".equals(job)) {
                emp.setJob("班主任");
            } else if ("3".equals(job)) {
                emp.setJob("就業指導");
            }
        });
        return empList;
    }
}

@Primary // このBeanを使う
@Service // このクラスをIOCコンテナに管理させ、IOCコンテナのBeanにする
public class EmpServiceB implements EmpService {
    
    @Autowired // 実行時、IOCコンテナがこの型のBeanオブジェクトを提供し、この変数に代入するんだ -- 依存性注入
    private EmpDao empDao;
    // private EmpDao empDao = new EmpDaoA();
    
    public List<Emp> listEmp(){
        List<Emp> empList = empDao.listEmp();
        empList.forEach(emp -> {
            String gender = emp.getGender();
            if("1".equals(gender)){
                emp.setGender("男");
            } else if ("2".equals(gender)) {
                emp.setGender("女");
            }
            String job = emp.getJob();
            if("1".equals(job)){
                emp.setJob("講師");
            } else if ("2".equals(job)) {
                emp.setJob("班主任");
            } else if ("3".equals(job)) {
                emp.setJob("就業指導");
            }
        });
        return empList;
    }
}

@Qualifier

どのBeanを使うか指定するアノテーションで、Controllerで使うよ。上の@Primaryアノテーションはキャンセルするね。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@RestController
public class EmpController {
    
    @Qualifier("empServiceA") // Bean `empServiceA`を使う
    @Autowired // 実行時、IOCコンテナがこの型のBeanオブジェクトを提供し、この変数に代入するんだ -- 依存性注入
    private EmpService empService;
    
    @RequestMapping("/listEmp")
    public Result listEmp(){
        
        //EmpService empServiceA = new EmpServiceA();
        //List<Emp> empList = empServiceA.listEmp();
        List<Emp> empList = empService.listEmp();
            
        return Result.success(empList);
    }
}

@Resource

@Qualifierと似てるんだけど、@Autowiredは使わないよ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@RestController
public class EmpController {
    
    // @Qualifier("empServiceA") // Bean `empServiceA`を使う
    // @Autowired 
    @Resource(name = "empServiceB") // Bean `empServiceB`を使う
    private EmpService empService;
    
    @RequestMapping("/listEmp")
    public Result listEmp(){
        
        //EmpService empServiceA = new EmpServiceA();
        //List<Emp> empList = empServiceA.listEmp();
        List<Emp> empList = empService.listEmp();
            
        return Result.success(empList);
    }
}

@Autowired@Resourceの違い

@AutowiredはSpringフレームワークが提供するアノテーションで、@ResourceはJDKが提供するアノテーションだよ。

@Autowiredはデフォルトで型に基づいて注入するけど、@Resourceはデフォルトで名前に基づいて注入するんだ。

Visits Since 2025-02-28

Hugo で構築されています。 | テーマ StackJimmy によって設計されています。