使用していないLiquibaseがなぜエラーを出すのか？

# Spring Bootアプリケーションの起動に失敗する謎のLiquibaseエラーを追跡する

## 1. はじめに

Spring Bootでアプリケーションを開発していると、思わぬエラーに遭遇することがあります。今回は、Spring BootとMyBatisを使ったプロジェクトで発生した「使用していないはずのLiquibaseに関するエラー」の原因究明と解決までの道のりを共有します。

この記事は、Spring Bootの自動設定の仕組みやBean初期化の順序について深く知りたい方、または同様の問題に直面している方の参考になれば幸いです。

## 2. 問題の発生

### エラーメッセージ

アプリケーション起動時に以下のようなエラーが発生しました。

```
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Failed to load ApplicationContext for [...]
...
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [com/burgerhub/config/MyBatisConfig.class]: Failed to instantiate [javax.sql.DataSource]: Factory method 'dataSource' threw exception with message: null
...
Caused by: java.lang.NoClassDefFoundError: liquibase/exception/ChangeLogParseException
```

奇妙なことに、プロジェクトではLiquibaseを明示的に使用しておらず、依存関係にも追加していませんでした。さらに、`application.yml`ファイルには次のような設定さえありました。

```yaml
spring:
  liquibase:
    enabled: false # Liquibaseの自動設定を明示的に無効化
```

### 設定ファイルと依存関係

プロジェクトの依存関係（build.gradle）:

```gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    // Liquibase依存関係はない
    // ...その他の依存関係
}
```

データソース設定（application.yml）:

```yaml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/burgerhub
    username: root
    password: mirai
    driver-class-name: com.mysql.cj.jdbc.Driver
```

## 3. 原因の調査

### 3.1 エラーログの分析

エラーメッセージを詳しく見ると、2つの問題が連鎖していることがわかります：

1. **Liquibaseのクラスが見つからないエラー**：
   ```
   java.lang.NoClassDefFoundError: liquibase/exception/ChangeLogParseException
   ```

2. **データソース構成の問題**：
   ```
   Error creating bean with name 'dataSource' ... Factory method 'dataSource' threw exception
   ```

これらの問題がなぜ発生するのか、そして互いにどう関連しているのかを調査する必要がありました。

### 3.2 解決への手がかり：MyBatisConfigファイルの変更

問題発生前と発生後のMyBatisConfigファイルを比較すると、重要な違いがありました。

**問題発生前のバージョン（正常に動作）**:

```java
@ConfigurationProperties("spring.datasource")
@Configuration
@MapperScan("com.burgerhub.mybatis.order")
@Data
public class MyBatisConfig {
    private String url;
    private String username;
    private String password;
    private String driverClassName;

    @Bean
    public DataSource dataSource() {
        return DataSourceBuilder.create()
                .url(this.url)
                .username(this.username)
                .password(this.password)
                .driverClassName(this.driverClassName)
                .build();
    }
    
    // ...その他のメソッド
}
```

**問題発生後のバージョン（エラーが発生）**:

```java
@Configuration
@MapperScan("com.burgerhub.mybatis.order")
public class MyBatisConfig {
    @Bean
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }
    
    // ...その他のメソッド
}
```

主な違いは：
1. `@ConfigurationProperties("spring.datasource")` アノテーションの削除
2. データソース設定用のフィールド（url、username、passwordなど）の削除
3. `DataSourceBuilder`へのパラメータ指定方法の変更

## 4. 問題解決への道

### 4.1 Liquibase依存関係の追加

最初に試したのは、エラーメッセージに表示されていたLiquibaseの依存関係を追加することでした。

```gradle
// Liquibaseの依存関係を追加
implementation 'org.liquibase:liquibase-core'
```

この修正により、`liquibase/exception/ChangeLogParseException`クラスが見つからないエラーは解消されました。しかし、次のエラーが発生しました：

```
dataSource or dataSourceClassName or jdbcUrl is required.
```

これは、データソースの接続情報が正しく設定されていないことを示しています。

### 4.2 根本原因の特定：初期化順序と自動設定

問題の根本原因は、Spring Bootの**初期化順序**と**自動設定の仕組み**にありました。

1. **@ConfigurationPropertiesの役割**：
   - このアノテーションは、外部設定ファイル（application.ymlなど）からプロパティを直接Javaクラスにバインドします
   - このバインディングはSpring Bootの起動プロセスの早い段階で行われます

2. **自動設定とデータソース**：
   - Spring Boot Data JPAは、データベースマイグレーションツールの自動設定を試みます
   - この自動設定プロセスでLiquibaseが検出されると、データソースに対して初期化を行おうとします

3. **初期化の順序**：
   - 古いバージョン：`@ConfigurationProperties`によりデータソース設定が早期に読み込まれ、明示的なDataSourceが構成される
   - 新しいバージョン：パラメータなしの`DataSourceBuilder.create().build()`はSpring Bootの自動設定に依存するが、設定が正しく読み込まれていない

### 4.3 最終解決策：@ConfigurationPropertiesの復活

根本原因を理解した上で、次のような修正を行いました：

```java
@ConfigurationProperties("spring.datasource")
@Configuration
@MapperScan("com.burgerhub.mybatis.order")
@Data
public class MyBatisConfig {
    private String url;
    private String username;
    private String password;
    private String driverClassName;

    @Bean
    public DataSource dataSource() {
        return DataSourceBuilder.create()
                .url(this.url)
                .username(this.username)
                .password(this.password)
                .driverClassName(this.driverClassName)
                .build();
    }
    
    // ...その他のメソッド
}
```

また、Liquibaseの依存関係も追加したままにしました：

```gradle
implementation 'org.liquibase:liquibase-core'
```

この修正により、アプリケーションは正常に起動するようになりました。

## 5. 考察：なぜこれで解決したのか？

### 5.1 @ConfigurationPropertiesと自動設定の順序

この問題を通じて理解できたのは、Spring Bootの初期化順序における`@ConfigurationProperties`の重要な役割です：

1. **早期のプロパティバインディング**：
   - `@ConfigurationProperties`は、自動設定が行われる前に適用されます
   - これにより、明示的な設定が自動設定よりも優先されることが保証されます

2. **明示的な設定 vs 自動設定**：
   - Spring Bootは「明示的な設定が自動設定に優先する」という設計原則を持っています
   - `@ConfigurationProperties`を使った設定は「明示的な設定」と見なされます

### 5.2 なぜLiquibaseが関係したのか？

Spring Boot Data JPAは、データベーススキーマ管理のために、クラスパス上にあるマイグレーションツールを自動的に構成しようとします：

1. Liquibaseが見つかれば、それを使用
2. 見つからなければ、Flywayを探す
3. どちらも見つからなければ、単にデータソースを使用

問題が発生したのは、Spring Boot Data JPAが自動設定プロセスでLiquibaseを検出しようとしたが、依存関係にLiquibaseが含まれていなかったためです。しかも、この検出は`liquibase.enabled=false`の設定が読み込まれる前に発生していました。

### 5.3 データソース設定とLiquibaseの関係

最も興味深いのは、データソース設定とLiquibaseエラーの関係です：

- 正しく設定されたデータソースが早期に初期化されると、自動設定プロセスはその既存のデータソースを使用します
- その結果、Liquibaseの自動設定は「既にデータソースが構成済み」という状態に対して行われるか、`liquibase.enabled=false`の設定が適用されます
- 一方、データソースが正しく構成されていないと、自動設定プロセスがLiquibaseを使ってデータソースを初期化しようとし、エラーが発生します

## 6. 教訓と学び

この問題から得られた教訓は以下の通りです：

1. **Spring Bootの自動設定を理解する**：
   - 自動設定は便利ですが、その仕組みと初期化順序を理解することが重要です
   - 問題が発生したときに、どのコンポーネントがどの順序で初期化されるかを知っておくと役立ちます

2. **@ConfigurationPropertiesの重要性**：
   - このアノテーションは単なる便利機能ではなく、初期化順序に影響を与える重要な役割を持ちます
   - 特にデータソースなどの基本的なインフラストラクチャコンポーネントを設定する際に有用です

3. **依存関係の明示化**：
   - Spring Bootが暗黙的に期待する依存関係があることを認識すべきです
   - 使用しない機能でも、エラーを避けるために依存関係を追加する必要がある場合があります

4. **トラブルシューティングの方法**：
   - エラーの連鎖を追跡することで根本原因を特定できることがあります
   - 「何が変わったか？」を常に考え、変更前後の差分を分析することが有効です

## 7. まとめ

今回の問題は、一見「使用していないLiquibaseがなぜエラーを出すのか？」という謎から始まりましたが、実際は「Spring Bootの初期化順序とデータソース設定」に関する問題でした。

このケースは、Spring Bootのような高度な自動設定を持つフレームワークでは、設定の仕方によって初期化の順序や動作が変わることを教えてくれます。また、フレームワークの内部動作を理解することが、エラーのトラブルシューティングにおいていかに重要かを示しています。

最後に、Spring Bootアプリケーションで似たような問題に直面した場合は、次の点を確認してみてください：

1. データソースなどの基本コンポーネントが正しく構成されているか
2. 自動設定が期待通りに動作しているか
3. 依存関係に不足はないか
4. 初期化の順序に問題はないか

これらの点を意識することで、より安定したSpring Bootアプリケーションを構築できると思います