MyBatis 환경 설정(SqlSessionFactory)

Violet_Evgadn·2023년 4월 23일
0

DB연동

목록 보기
6/22

들어가기 앞서...

이전에 우리는 MyBatis를 실행하기 위해서는 SqlSessionFactory라는 객체가 존재해야 하고, SqlSessionFactory가 매 Request마다 SqlSession을 만들어서 Query를 수행함을 배웠다.

따라서 이번 Section에서는 MyBatis의 주체라고도 말할 수 있는 SqlSessionFactory에 대한 설정을 해보겠다.


사전 작업(이전 Section 참고)

먼저 application.properties의 설정은 이전 Section과 마찬가지로 수행해준다. 우리는 MySQL을 활용하기로 하고 현재 프로젝트 구조는 아래와 같다고 가정하자

# application.properties 설정
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/database
spring.datasource.username=root
spring.datasource.password=root

mybatis.type-aliases-package=egovframework.com.review.mapper
mybatis.mapper-locations=egovframework/sqlmap/mysql/**/*.xml

SqlSessionFactory Bean 등록

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class MyBatisConfiguration {
    @Value("${mybatis.mapper-locations}")
    String mPath;
    
    @Value("${mybatis.type-aliases-package}")
    String package_name;


    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource(){
        return DataSourceBuilder.create().build();
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource, ApplicationContext applicationContext) throws Exception{
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(mPath));
        sqlSessionFactoryBean.setTypeAliasesPackage(package_name);
        return sqlSessionFactoryBean.getObject();
    }

}

위 클래스 파일이 SqlSessionFactory를 생성하기 위한 클래스이다. 하나씩 차근차근 알아보자

mPath

mybatis.mapper-locations에 저장되어 있는 값을 가지고 온 것이다.

이전에 mybatis.mapper-locations는 "Mapper Interface와 대응되는 Query문을 저장한 파일"에 대한 경로를 입력했다.

즉 mPath에는 Mapper Interface가 실행할 실제 Query문을 저장해 놓은 XML 파일 경로가 저장되어 있는 것이다.

DataSource

이전에 말했듯 JDBC에서 직접 DB와 연결해줬던 과정을 MyBatis에서는 DataSource라는 객체가 대신 수행해준다.

따라서, application.properties에 지정했던 "DB와 관련된 설정값"들을 모두 DataSource 객체를 만들 때 적용시켜 생성해야 할 것이다.

이를 위해 @ConfigurationProperties(prefix="spring.datasource") 구문을 활용하여 application.properties에 존재하는 spring.datasource로 시작하는 설정값들을 모두 가지고온다. 위 application.properties 파일을 보면 알 수 있듯 "spring.datasource"로 시작하는 설정값들은 모두 내가 사용할 DB와 관련이 있는 정보라는 것을 알 수 있다.

내가 연결하여 사용할 DB에 대한 설정값을 활용해 DataSource 객체를 만들고, 이를 Bean으로 등록함으로써 SqlSessionFactory 측에서 Bean으로 등록한 DataSource 객체를 활용할 수 있게 되는 것이다

SqlSessionFactory

이 Section의 핵심 부분이다. SqlSessionFactory 생성 부분이다.

SqlSessionFactory는 SqlSessionFactoryBean 객체를 먼저 형성하고, 이 Bean 객체에 여러가지 설정 값들을 넣은 뒤 해당 설정을 포함하는 SqlSessionFactory를 Bean이 반환하는 형식으로 만들어진다.

<이 부분은 개인적인 생각, 즉 뇌피셜이므로 넘어가셔도 됩니다>
Bean이라는 것이 결국 스프링 컨테이너에 저장할 객체를 만들기 위한 중간 과정이 포함된 메서드라는 것으로 생각해볼 때 SqlSessionFacoryBean도 SqlSessionFactory를 위한 설정값을 받고 이 설정값이 온전히 적용될 수 있도록 중간 과정을 수행해주는 객체가 아닐까라는 생각을 해보았다.
SqlSessionFactoryBean이 설정값을 이미 구현된 로직을 통해 SqlSessionFactory에 먹여줌으로써 Human Error를 줄이면서 더욱 확실하게 설정값을 먹여줄 수 있기 때문에 이런 방법을 쓰는 것이 아닐까 라는 생각을 했다.
실제로 소스 코드를 보았을 때 getObject() 메서드를 보면 "afterProperties()"라는 메서드를 호출하고, 이 메서드는 "buildSqlSessionFactory()"를 호출함을 알 수 있다. "buildSqlSessionFactory()"는 결국 우리가 설정했던 설정값을 SqlSessionFactory에 먹이는 과정이 포함되어 있음을 볼 때 내가 생각한 것이 맞을 것 같았다.
하지만, 이를 뒷받침해줄 사이트가 없었기 때문에(검색 능력의 부족...) 일단 뇌피셜이라고 생각해주면 될 것 같다.

내가 생각하는 중요한 설정 인자는 아래 4가지이다.

DataSource

우리가 위에서 지정했던 DataSource가 sqlSessionFactory 메서드의 Input으로 들어오고, 해당 DataSource를 적용해야지만 우리가 원하는 DB와 연동이 가능해질 것이다.

"우리가 위에서 Bean으로 등록한" DataSource라는 것을 명심하자.

MapperLocation

setMapperLocations에서 Input으로 무엇을 받는지 확인하자

applicationContext.getResource라는 부분이 존재하긴 하지만, 결국은 "mPath"라는 값을 입력받는 것이다.

여기에서 applicationContex.getResource라는 부분도 생각보다 중요한 부분이긴 한데 일단 MyBatis와 큰 연관은 없으므로 넘어가자.(나중 Section에서 다뤄야겠다. 일단 지금은 "resources" 폴더 경로를 시작 경로로 만들어주는 메서드라고 생각하면 된다.)

이전에 말했듯 mPath는 "Mapper Interface가 실행할 Query문을 저장한 파일들"의 경로를 저장하고 있으며, MapperLocation은 이런 Query문을 저장한 XML 파일들의 경로에 대한 설정값이라고 할 수 있다.

TypeAliasesPackage

이 설정값을 보고 "Type Alias는 application.properties에서 설정하지 않았었나?"라고 생각한 당신은 천재!

TypeAliasesPackage는 application.properties에서 지정해줬던 mybatis.type-aliases-package의 역할을 하는 설정값이다.

그런데 왜 이 값을 중요하다고 생각하였는가?

바로 Error 발생에 대한 해결 방법이다.

구글링을 해도 모르겠어서 이유까지는 모르겠지만 분명 type-aliases-package를 application.properties에 설정했음에도 불구하고 Query문을 저장한 XML 파일 쪽에서 VO 역할을 할 객체(resultType) 경로를 못 받아와서 에러가 발생하는 상황이 발생했다.

즉, applicatoin.properties에 적었던 내용이 제대로 적용되지 않았다는 의미인 것이다.

이 부분을 위해서 MyBatis의 Query문 1개만 보도록 하자.(나중에 Query문 입력 방법도 자세히 설명할테니 지금은 이해보다는 눈으로 보기만 하자!)

<select id="findAll" resultType="Member">
    select * from example
</select>

위 예시는 example이라는 Table의 각 Row에 저장된 Data를 Member라는 객체로 저장하고, 모든 Row Data를 List 형태로 저장해서 반환하는 Query문이다.

여기에서 resultType이 그냥 "Member"라고만 지정되어 있는데, 원래라면 이렇게만 지정해 놓았을 경우 "어떤 경로에 있는 Member 객체인가"를 인지하지 못한다. 하지만 만약 type-aliases-package를 활용한다면 지정한 패키지 아래에 존재하는 파일들 중 Member라는 이름의 객체를 찾아 자동으로 매칭시켜준다.

문제는 application.properties에 type-aliases-package를 설정해주면 가끔 에러가 발생한다는 것이다. 에러의 정확한 이유는 모르겠으나, 이런 에러를 피하기 위해서는 resultType을 "egovframework.com.review.mybatis.Member"와 같이 해당 객체의 전체 Path를 매번 입력해줘야 하는 것이다.

와 상상만 해도 귀찮은 작업이다!

따라서 setTypeAliasesPackage 메서드를 통해 application.properties에 지정한 type-aliases-package 설정값을 SqlSessionFactoryBean에 직접 먹여줌으로써 에러의 발생확률을 없애는 것이다.

setTypeAliasesPackage는 알아서 resources 폴더와 동일 Depth에 존재하는 java 폴더를 시작 위치로 잡아주기 때문에 MapperLocation처럼 applicationContext라던가 classpath:/ 를 활용하지 않아도 된다

위 코드처럼 @Value와 setTypeAliasesPackage를 통해 설정해주게 되면 에러를 걱정할 필요 없이 resultType에 편하게 객체 이름만 적어도 되는 것이다

(물론, 이전 Section에서도 말했듯이 절대로 중복된 클래스 이름이 존재해서는 안된다)

ConfigurLocation

위에는 따로 지정하지 않았던 설정값이다. 하지만 꽤 재미있는 기능인 것 같아 한 번 조사해보았다.

MyBatis에서는 < settings >를 통해 런타임시 MyBatis 행위를 조정하기 위한 설정값들이 존재한다. 예를 들어 cacheEnabled라는 Mapper의 캐시 사용 여부에 대한 설정값이라던가 lazyLoadingEnabled라는 지연 로딩(Lazy Loading)의 활용 여부 등에 대한 설정값이 존재한다.

이런 Setting들에 대한 XML 파일을 MyBatis에 실제로 적용시키기 위해서는 properties에 해당 파일을 명시해줘야 하는데, 굳이 properties에 명시하지 않고 ConfigLocation을 통해 설정파일을 명시해 줄 수 있다.

사용 방법은 아래와 같다.

Resource configLocations 
       				= new ClassPathResource("/egovframework/sqlmap/config/sql-map-config.xml");

sqlSessionFactoryBean.setConfigLocation(configLocations);

sql-map-config.xml 파일에 MyBatis에 대한 설정값들이 저장되어 있다면 해당 XML 파일에 저장된 설정값들을 가지고 와서 SqlSessionFactoryBean에 등록해줄 수 있는 것이다. 자연스럽게 이런 Setting들은 SqlSessionFactory에 적용되게 될 것이다.

번외로 < typeAliases >를 설정 파일(sql-map-config.xml 파일)에 명시함으로써 Query를 저장한 모든 XML 파일에서 typeAlias를 입력하지 않고 일괄적으로도 적용할 수 있는데, 위에서 TypeAliasesPackage라는 좋은 설정 기능이 있으니 이를 활용하는 것이 좋아보인다(물론 < typeAlias >는 직접 path를 알려주므로 속도가 더 빠를 수는 있겠지만, 개인적인 생각으론 이 시간이 0.x일거라는 생각이 드는데 대회를 하는것이 아니라면야 이 시간이 큰 의미가 있을까 의문이며 이 방식을 활용하면 Class 1개를 추가할 때마다 sql-map-config.xml 파일에 < typeAlias >를 추가해줘야하므로 유지보수에도 많은 어려움이 존재할 것 같다)


DataSource

먼저 DataSource 객체는 위에 존재하는 코드처럼 DataSourceBuilder.create().build()를 통해 정말 손쉽게 만들 수 있다. 또한 위 방식이 범용성, 유지보수성, 확장성 등 모든 면에서 우월하다고 생각한다. 즉 아래에서 설명하는 것처럼 DataSource Builder를 활용하지 않고 직접 DataSource 객체를 생성하여 빈으로 등록하고 활용하는 것은 '굳이...?'라는 의문이 나오게 한다.

하지만 저는 해보겠습니다. 변태는 아니고 그냥 공부를 하다보니 이런 방법도 있다는 것을 알게 되어 기입하는 것이니 큰 걱정하지 않아도 된다.

import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DataSourceContextConfiguration {

    @Value("${spring.datasource.url}")
    String url;
    @Value("${spring.datasource.username}")
    String username;
    @Value("${spring.datasource.password}")
    String password;
    @Value("${spring.datasource.driver-class-name}")
    String driverClassName;
    @Value("${spring.datasource.platform}")
    String platform;


    @Bean(name = "dataSource", destroyMethod = "close")
    public BasicDataSource dataSource(){
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(this.driverClassName);
        dataSource.setUrl(this.url);
        dataSource.setUsername(this.username);
        dataSource.setPassword(this.password);

        if(this.platform.equals("oracle")){
            dataSource.setValidationQuery("SELECT 1 FROM DUAL");
        }
        if(!this.platform.equals("mysql")){
            dataSource.setValidationQuery("SELECT 1");
        }

        return dataSource;
    }
}

먼저 위 예시를 보자. @Value를 통해 spring.datasource.username, spring.datasource.password, spring.datasource.url, spring.datasource.driver-class-name, spring.datasource.platform을 application.properties 파일에서 받아와 활용함을 알 수 있다. 그리고 이 부분 때문에 유지보수성이 매우 떨어지는 것이다.

예를 들어보자. 위에서는 spring.datasource.url을 활용하는데, 서버를 이동시킨 이후 application.properties에는 spring.datasource.jdbc-url을 활용한다고 가정하자. 그렇게 되면 에러가 발생하게 될 것이고 이 에러를 찾다보면 spring.datasource.url이 문제라는 것을 알 수 있을 것이다.

그렇다면 spring.datasource.jdbc-url을 spring.datasource.url로 바꾸면 되지 않나?

이는 매우 단편적인 생각이다. 만약 다른 부분에서 @Value(${spring.datasource.jdbc-url})을 활용한다면 또 에러가 발생하게 될 것이다.

그렇다면 둘 다 살리면 되지 않나?

동작은 될 것이다. 문제는 만약 Oracle에서 MySQL로 DBMS를 바꾼다던가, DB의 IP 주소가 바뀌었다든가, Table의 이름이 바뀌는 상황에는 두 설정값 모두 변경시켜줘야 하고, 동일한 값을 가져야 할 것이다. 이는 곧 유지보수의 난이도를 올리기 때문에 변경성에 매우 취약하게 됨을 의미한다.

이제 왜 이런 단점을 가지는지 알았으니 활용 방식에 대해 알아보자. 활용방식은 간단하다.

Setter 함수를 통해 DriverClassName, URL, Username, Password를 주입해주면 된다. if문을 통해 dataSource.setValidationQuery를 수행하는데, 실제 DB와 잘 연동되었는지 Test하는 쿼리를 수행시켜 성공여부를 확인하는 메서드라고 이해하면 된다.

DataSourceBuilder는 이 모든 과정을 자동화 시켰기 때문에 확장성, 유연성, 유지보수성 모두 뛰어날 것이고 Human Error의 발생확률도 매우 줄여주므로 Builder를 활용하도록 하자


다음 Section으로 가기에 앞서...

자 그럼 이제 설정은 끝났다. 사실 Spring이라는 것이 워낙 편한 프레임워크라 그런지 사용은 어렵지가 않은데 설정이 어렵다.

내가 느끼기로는 100의 작업을 해야하면 80은 설정이고 20은 개발 과정이 아닐까 생각이 들 정도다.

꼭 성공적으로 설정이 되었기를 바라며 다음 Section에서는 대략적인 MVC 패턴을 활용해서 Mapper를 활용해보자.

profile
혹시 틀린 내용이 있다면 언제든 말씀해주세요!

0개의 댓글