MapperScannerConfigurer 설정 오류로 인한 데이터베이스 연결 속성 로드 실패

MyBatis와 Spring 프레임워크를 통합하여 애플리케이션을 개발할 때, 데이터베이스 연결과 관련된 설정 오류는 흔히 발생할 수 있습니다. 특히 MapperScannerConfigurer의 구성 방식에 따라 db.properties 파일에 정의된 데이터베이스 연결 매개변수가 제대로 로드되지 않아 JDBC 드라이버 초기화에 실패하는 경우가 있습니다.

문제 현상

애플리케이션을 시작할 때, 다음과 같은 유형의 예외가 발생하며 데이터베이스 연결에 실패하는 현상이 나타날 수 있습니다. 로그에는 ${jdbc.driverClass}와 같은 플레이스홀더 변수가 실제 값으로 치환되지 않은 채로 드라이버 클래스를 찾으려는 시도가 기록됩니다. 이는 스프링 컨텍스트가 속성 파일을 로드하여 플레이스홀더를 처리하기 전에 특정 빈이 너무 일찍 초기화되었음을 의미합니다.

09:59:06.595 [C3P0PooledConnectionPoolManager[identityToken->z8kfltb71qnbl7e1cco0kz|23833818]-HelperThread-#2] WARN  c.m.v2.c3p0.DriverManagerDataSource - Could not load driverClass ${jdbc.driverClass}
 java.lang.ClassNotFoundException: ${jdbc.driverClass}
     at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1332)
     at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1144)
     at java.base/java.lang.Class.forName0(Native Method)
     at java.base/java.lang.Class.forName(Class.java:534)
     at java.base/java.lang.Class.forName(Class.java:513)
     at com.mchange.v2.c3p0.DriverManagerDataSource.loadDriverClass(DriverManagerDataSource.java:129)
     at com.mchange.v2.c3p0.DriverManagerDataSource.ensureIfPossibleDriverClassLoaded(DriverManagerDataSource.java:108)
     at com.mchange.v2.c3p0.DriverManagerDataSource.getConnection(DriverManagerDataSource.java:157)
     at com.mchange.v2.c3p0.WrapperConnectionPoolDataSource.getPooledConnection(WrapperConnectionPoolDataSource.java:167)
     at com.mchange.v2.c3p0.WrapperConnectionPoolDataSource.getPooledConnection(WrapperConnectionPoolDataSource.java:153)
     at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool$1PooledConnectionResourcePoolManager.acquireResource(C3P0PooledConnectionPool.java:499)
     at com.mchange.v2.resourcepool.BasicResourcePool.doAcquire(BasicResourcePool.java:1112)
     at com.mchange.v2.resourcepool.BasicResourcePool.doAcquireAndDecrementPendingAcquiresWithinLockOnSuccess(BasicResourcePool.java:1099)
     at com.mchange.v2.resourcepool.BasicResourcePool.access$700(BasicResourcePool.java:9)
     at com.mchange.v2.resourcepool.BasicResourcePool$ScatteredAcquireTask.run(BasicResourcePool.java:1846)
     at com.mchange.v2.async.ThreadPoolAsynchronousRunner$PoolThread.run(ThreadPoolAsynchronousRunner.java:696)

ClassNotFoundException에 이어서 다음과 같은 SQLException도 발생할 수 있습니다.

Caused by: java.sql.SQLException: No suitable driver
    at java.sql/java.sql.DriverManager.getDriver(DriverManager.java:300)
    at com.mchange.v2.c3p0.DriverManagerDataSource.driver(DriverManagerDataSource.java:273)
    at com.mchange.v2.c3p0.DriverManagerDataSource.getConnection(DriverManagerDataSource.java:159)
    at com.mchange.v2.c3p0.WrapperConnectionPoolDataSource.getPooledConnection(WrapperConnectionPoolDataSource.java:167)
    at com.mchange.v2.c3p0.WrapperConnectionPoolDataSource.getPooledConnection(WrapperConnectionPoolDataSource.java:153)
    at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool$1PooledConnectionResourcePoolManager.acquireResource(C3P0PooledConnectionPool.java:499)
    at com.mchange.v2.resourcepool.BasicResourcePool.doAcquire(BasicResourcePool.java:1112)
    at com.mchange.v2.resourcepool.BasicResourcePool.doAcquireAndDecrementPendingAcquiresWithinLockOnSuccess(BasicResourcePool.java:1099)
    at com.mchange.v2.resourcepool.BasicResourcePool.access$700(BasicResourcePool.java:9)
    at com.mchange.v2.resourcepool.BasicResourcePool$ScatteredAcquireTask.run(BasicResourcePool.java:1846)
    at com.mchange.v2.async.ThreadPoolAsynchronousRunner$PoolThread.run(ThreadPoolAsynchronousRunner.java:696)

환경 설정 예시

일반적인 Spring + MyBatis 통합 환경 (SSM 스택)의 주요 구성 요소는 다음과 같습니다.

Maven 의존성 (pom.xml)

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.17</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.4</version>
        </dependency>
        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.10.1</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>9.1.0</version>
            <scope>runtime</scope>
        </dependency>

데이터베이스 연결 속성 (db.properties)

jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.jdbcUrl=jdbc:mysql://127.0.0.1/java
jdbc.user=root
jdbc.password=root

스프링 설정 (spring.xml)

문제 발생 시의 스프링 XML 설정은 다음과 같았습니다:

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- DAO 패키지 컴포넌트 스캔 활성화 -->
    <context:component-scan base-package="cn.oraenv.javaweb.dao"/>

    <!-- AspectJ 자동 프록시 활성화 -->
    <aop:aspectj-autoproxy/>

    <!-- db.properties 파일에서 플레이스홀더 속성 로드 -->
    <context:property-placeholder location="classpath:db.properties"/>

    <!-- C3P0 데이터 소스 설정 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driverClass}"/>
        <property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>
        <property name="user" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- 트랜잭션 매니저 설정 -->
    <bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 애노테이션 기반 트랜잭션 활성화 -->
    <tx:annotation-driven/>

    <!-- MyBatis SqlSessionFactory 설정 -->
    <bean name="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>

    <!-- 문제의 MapperScannerConfigurer 설정 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- sqlSessionFactory를 ref로 지정하는 것이 문제의 원인 -->
        <property name="sqlSessionFactory" ref="sqlSessionFactoryBean"/>
        <property name="basePackage" value="cn.oraenv.javaweb.dao"/>
    </bean>
</beans>

문제 분석

예외 로그를 분석해 보면, JDBC 드라이버 클래스 이름이 db.properties 파일의 실제 값으로 치환되지 않고 플레이스홀더인 ${jdbc.driverClass} 그대로 사용된 것을 확인할 수 있습니다. 이는 스프링 컨텍스트가 속성 파일을 로드하여 플레이스홀더를 처리하는 시점보다 특정 빈이 너무 일찍 초기화되었기 때문에 발생합니다.

이 문제의 원인은 MapperScannerConfigurer의 설정 방식에 있습니다. MapperScannerConfigurer는 Spring의 BeanFactoryPostProcessor 인터페이스를 구현하며, 이는 다른 일반 빈 정의들이 로드되고 처리되기 전에 실행됩니다. 만약 sqlSessionFactory 속성을 ref 방식으로 지정하면, MapperScannerConfigurerSqlSessionFactoryBean 빈을 즉시 참조하여 초기화하려고 시도합니다.

그러나 이 시점에는 <context:property-placeholder> 태그에 의해 db.properties 파일의 내용이 아직 스프링 환경에 로드되지 않은 상태입니다. 따라서 SqlSessionFactoryBean이 의존하는 dataSource 빈이 초기화될 때, ${jdbc.driverClass}와 같은 플레이스홀더가 실제 값으로 치환되지 않아 드라이버 클래스를 찾을 수 없다는 ClassNotFoundException이 발생하는 것입니다.

해결 방안

MapperScannerConfigurer를 올바르게 설정하려면, sqlSessionFactory 빈 자체를 직접 참조하는 대신 sqlSessionFactoryBeanName 속성을 사용하여 해당 빈의 ID를 문자열로 전달해야 합니다. sqlSessionFactoryBeanName을 사용하면 MapperScannerConfigurer는 빈 정의를 수정하는 역할만 수행하고, 실제 SqlSessionFactoryBean의 인스턴스화는 모든 속성 플레이스홀더가 처리된 후 일반적인 빈 라이프사이클에 따라 진행됩니다.

MyBatis-Spring 공식 문서에서는 MapperScannerConfigurer에 대해 다음과 같은 설정 방법을 제시합니다:

  1. 스프링 컨텍스트 내에 SqlSessionFactory 빈이 하나만 존재한다면, sqlSessionFactory 또는 sqlSessionFactoryBeanName 속성을 명시적으로 설정할 필요가 없습니다. MapperScannerConfigurer가 자동으로 유일한 SqlSessionFactory를 찾아서 사용합니다.
  2. 스프링 컨텍스트 내에 여러 개의 SqlSessionFactory 빈이 존재한다면, sqlSessionFactoryBeanName 속성을 사용하여 해당 SqlSessionFactory 빈의 이름을 value 방식으로 주입해야 합니다.

따라서, 기존의 spring.xml 설정에서 MapperScannerConfigurer 부분을 다음과 같이 수정합니다.

    <!-- 수정된 MapperScannerConfigurer 설정 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- sqlSessionFactoryBean의 ID를 문자열로 지정하여 플레이스홀더 처리가 완료된 후 초기화되도록 합니다. -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/>
        <property name="basePackage" value="cn.oraenv.javaweb.dao"/>
    </bean>

이 변경을 적용하면 db.properties 파일의 데이터베이스 연결 파라미터가 올바르게 로드되며, JDBC 드라이버 관련 오류 없이 애플리케이션이 정상적으로 시작됩니다.

태그: MyBatis Spring Framework MapperScannerConfigurer SQL JDBC

7월 21일 23:29에 게시됨