런타임 시 마이바티스의 행위를 조정하기 위한 값들이다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="cacheEnabled" value="false" />
<setting name="lazyLoadingEnabled" value="true" />
<setting name="multipleResultSetsEnabled" value="true" />
<setting name="useColumnLabel" value="true" />
<setting name="useGeneratedKeys" value="false" />
<setting name="enhancementEnabled" value="false" />
<setting name="defaultExecutorType" value="SIMPLE" />
<setting name="defaultStatementTimeout" value="25000" />
<setting name="safeRowBoundsEnabled" value="false" />
<setting name="mapUnderscoreToCamelCase" value="false" />
<setting name="localCacheScope" value="SESSION" />
<setting name="jdbcTypeForNull" value="OTHER" />
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString" />
</settings>
</configuration>
먼저 위 파일과 같이 XML Config 파일을 만들어줘야 한다.
방법은 간단한데 < configuration> ~ < /configuration>으로 SQL이 아닌 Config에 대한 파일임을 알려주고, < settigns>를 통해 우리가 원하는 설정값들을 지정해주면 된다.
SqlSessionFactory 설정을 설명할 때 setConfigLocation()에 대해 설명한 적이 있었는데 바로 이 메서드를 활용할 차례이다. 하드 코딩을 하거나 application.properties에 Config XML 파일의 경로를 입력해준 뒤 경로를 SqlSessionFactoryBean에 먹여주면 된다. 이후 SqlSessionFactory는 해당 설정을 가지는 SqlSession을 생성하여 Query문을 처리할 것이다.
방법은 아래와 같다
@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);
////////////// 추가된 부분 ///////////////////
Resource configLocations = new ClassPathResource("/egovframework/sqlmap/"+ this.platform + "/config/sql-map-config.xml");
sqlSession.setConfigLocation(configLocations);
/////////////////////////////////////////////
sqlSessionFactoryBean.setTypeAliasesPackage(package_name);
sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(mPath));
return sqlSessionFactoryBean.getObject();
}
}
configLocations가 String이 아닌 Resource 객체임을 유의하자.
이렇게 setting을 통해 설정할 수 있는 설정값들은 많이 존재한다. 이 값들에 대해 알아보자
설명할 형식은 아래와 같다.
{Setting Name} : {Value로 들어갈 수 있는 값들}
- 설명
- Default Value
cacheEnabled : {true, false}
lazyLoadingEnabled : {true, false}
aggressiveLazyLoading : {true, false}
multipleResultSetsEnabled : {true, false}
useColumnLabel : {true, false}
useGeneratedKeys : {true, false}
autoMappingBehavior : {NONE, PARTIAL, FULL}
autoMappingUnknownColumnBehavior : {NONE, WARNING, FAILING}
defaultExecutorType : {SIMPLE, REUSE, BATCH}
defaultStatementTimeout : {양수}
safeRowBoundsEnabled : {true, false}
mapUnderscoreToCamelCase : {true, false}
localCacheScope
jdbcTypeForNull : {JdbcType Enum. 대부분 NULL, VARCHAR, OTHER를 활용}
lazyLoadTriggerMethods : {메서드 이름을 나열하고, 여러 개일 경우 콤마(,)로 구분}
callSettersOnNulls : {true, false}
logPrefix : {문자열}
logImpl : {SLF4J, LOG4J2, JDK_LOGGIN, COMMONS_LOGGING, STDOUT_LOGGING, NO_LOGGING}
proxyFactory : {CGLIB | JAVASSIST}
이외에도 여러 가지 설정이 있지만, 외울 생각은 하지 말고 이런 게 있구나 정도로 넘어가면 된다.
타입 별칭은 자바 타입(클래스)에 대한 짧은 이름이다. 오직 XML 설정에서만 활용되며 타이핑을 줄이기 위해 존재한다.
예를 들어 <select id="selectBannerList" parameterType="Author">
이라고 가정하자.
그런데 XML 파일에서는 Author라는 객체가 어디에 존재하는지 알 수가 없기 때문에 모든 경로(ex. domain.blog.Author)를 입력해줘야 한다. 만약 Author 객체를 활용할 때마다 풀 경로를 매번 입력하려면 엄청난 타이핑양이 필요할 것이며 유지보수도 힘들어질 것이다.
따라서 우리는 이를 typeAliases로 설정해줌으로써 클래스 이름, 즉 Author만 입력해도 자동으로 domain.blog.Author라는 것을 인식할 수 있도록 별칭을 설정해주는 것이다.
이전 설정 Section 중 application.properties에서 mybatis.type-aliases-package, 혹은 setTypeAliasesPackage()를 통해 설정해주었지만, Config 파일을 하나 파서 < typeAlias>를 모두 입력해주는 것도 가능하다.
(위에서 설정했던 < configuration> ~ </ configuration> 사이에 추가해줄 수 있음)
<typeAliases>
<typeAlias type="com.example.Capability" alias="Capability" />
<typeAlias type="com.example.Role" alias="Role" />
</typeAliases>
먼저 type은 별칭과 연결될 실제 클래스의 경로이며, alias는 별칭이다. 이때 type는 "src/main/java"를 시작 Path로 잡고 Search를 시작하기 때문에, java 아래 경로만 입력해주면 된다.
위처럼 설정하면 com.example.Capability는 앞으로 Capability라는 이름으로, com.example.Role은 앞으로 Role이라는 짧은 이름으로 대체하여 활용할 수 있게 되는 것이다.
<typeAliases>
<package name="com.example">
</typeAliases>
< typeAlias>를 활용하는 것은 직관적일 수는 있지만 개발 시 클래스 1개를 추가할 때마다 typeAlias를 추가해줘야 함을 알 수 있다. 예를 들어, com.example.Member를 넣고 싶다면 <typeAlias type="com.example.Member" alias="Member"/>
구문을 추가해줘야 하는 것이다.
이런 귀찮음을 줄이기 위해 패키지 내에 존재하는 모든 클래스를 자동으로 typeAlias 설정할 수 없을까?라는 생각으로 나온 것이 < package>이다.
< package name="com.example">로 지정하면 com.example 아래 존재하는 모든 클래스를 < typeAlias>로 자동 등록해주는 것이다. 클래스에 @Alias 어노테이션이 없다면 클래스를 모두 소문자로 변환한 형태로 Alias가 지정되고(Author 같은 경우는 author이 될 것이다), 클래스에 @Alias("Author")
처럼 어노테이션을 붙여준다면 어노테이션에 지정한 값으로 Alias가 지정될 것이다.
솔직히 이 방법을 추천하지는 않는데, 일단 package 아래 있는 모든 클래스를 typeAlias로 지정하기 때문에 쓰지 않는 클래스들도 다 Alias로 등록된다는 단점이 존재한다. 또한 @Alias()를 붙여줘서 내가 원하는 이름으로 별칭을 지정하는 방법도 있고 기본값인 소문자로 변환된 값을 별칭으로 설정하는 방법도 있는데, 여러 명이 개발을 해서 반은 @Alias, 반은 Default인 소문자로 지정하게 된다면 유지보수가 어마어마하게 힘들어질 것 같다.
그래서 차라리 조금 귀찮지만 < typeAlias>를 추천한다.
위 방식들을 활용하면 < typeAliases>를 기입한 XML 파일에만 Alias를 활용할 수 있게 된다. 따라서, 위에서 말했듯 그냥 < configuration> ~ < /confiugration>에 < typeAliases>에 대한 모든 설정을 넣고 이렇게 만들어진 Config 파일을 SqlSessionFactoryBean에 먹여줌으로써 모든 XML 파일에 대하여 동일한 typeAliases를 적용할 수도 있다.
물론 우리는 이전에도 활용했듯 setTypeAliasesPackage()를 통해 쉽고 유지보수도 원활하게 설정할 수 있었기 때문에 이 방법을 활용하도록 하자.
(setTypeAliasesPackage 또한 < package>처럼 쓸모없는 클래스들도 다 Alias로 등록된다는 단점은 존재하지만 유지보수가 정말 매우 쉬워지기 때문에 조금의 손해를 감수하더라도 활용할 가치가 있다고 생각한다)
MyBatis가 ResultSet에서 값을 가져올 때마다 Value가 적절한지 확인하기 위해 활용되는 것이다. 적절하다는 것은 "내가 원하는 Java Type으로 변환할 수 있다는 것이다.
예를 들어, 나는 Boolean형 데이터를 원하는데 "WOW"라는 값이 오면 "WOW"는 true나 false로 변환 불가능하기 때문에 에러를 발생시켜야 하며, 이를 위해 활용하는 것이 TypeHandler이다.
Default TypeHandler가 존재하며 TypeHandler를 Override 하여 직접 TypeHandler를 만들어줄 수도 있다.
<result typeHandler="">
로 지정해줌으로써 객체의 멤버 변수(Table의 Column값)마다 typeHandler를 적용해주거나 Instance(Table Row Data)마다 다른 TypeHandler를 적용하여 객체의 특성에 맞도록 Value 적절성 여부를 판단하게 활용할 수 있을 것이다.
public abstract class AbstractCipherTypeHandler implements TypeHandler<String> {
@Override
public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
if(isCipher()){
// isCipher() = true일 경우 Data를 암호화 함
parameter = encode(parameter);
}
// 만약 if문에서 암호화를 수행했다면 암호화된 데이터가 저장될 것이다.
ps.setString(i, parameter);
}
@Override
public String getResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
if(isCipher()){
// 암호화되어 있는 데이터라면 복호화 함
value = decode(value);
}
return value;
}
@Override
public String getResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
if(isCipher()){
// 암호화되어 있는 데이터라면 복호화 함
value = decode(value);
}
return value;
}
@Override
public String getResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
if(isCipher()){
// 암호화되어 있는 데이터라면 복호화 함
value = decode(value);
}
return value;
}
// isCipher()이 True를 반환하면 암호화하고 싶은 Data라는 것을 의미함
protected abstract boolean isCipher();
// 암호화 과정
protected String encode(String value){
try{
value = Base64.encode(value);
}catch(Exception e){}
return value;
}
// 복호화 과정
protected String decode(String value){
try{
value = Base64.decode(value);
}catch(Exception e){}
return value;
}
}
public class EmailCipherTypeHandler extends AbstractCipherTypeHandler {
@Override
protected final boolean isCipher() {
return true;
}
}
/*
* 위에서 지정했던 AbstractCipherTypeHandler를 상속받고 isCipher()가 true를 반환하게 했다.
* 즉, EmailCipherTypeHandler는 DB에 "암호화된 데이터를 저장"하는 로직을 가지게 된다.
*
* 만약 <result typeHandler="EmailCipherTypeHandler">로 지정한다면 해당 Column에 저장될
* 데이터는 암호화된 이후 DB에 저장되게 될 것이며 Column에 저장된 데이터를 가지고 올 때는
* 복호화를 수행하게 될 것이다.
/
자 여기에서 TypeHandler의 진수가 나온다.
먼저 TypeHandler는 "Query의 결과가 내가 지정한 Java Type과 일치하는가" 여부를 확인하여 적절한 Value를 가지고 오기 위해 명시해주는 것임을 알고 있다. 그렇다면 Query의 결과가 Java Type과 연동될지를 확인하기 위해서는 Query의 결과를 원하는 객체 멤버 변수에 값을 저장하기 전에 미리 뽑아볼 필요가 있다는 것이다.
예를 들어 Query의 결과가 {"ABC", "Male", "111-111"}이라고 하고 MyBatis는 Member라는 객체에 이 정보를 담아서 반환한다고 가정하자. 그런데 TypeHandler가 설정되어 있다면 {"ABC", "Male", "111-111"}라는 값을 Member 객체의 멤버 변수에 저장하기 이전에 "ABC", "Male", "111-111"이라는 값이 "각각의 Data가 내가 사전에 지정했던 Java Type과 일치하는가" 여부를 먼저 확인하고, 사전에 지정했던 Java Type과 일치함을 인지한 이후에 Member 객체의 멤버 변수에 값을 저장해야 할 것이다.
즉, TypeHandler는 Query문의 순수한 결과물을 확인할 수 있는 유일한 중간 과정이며, 동시에 객체를 DB에 저장할 때 멤버 변수에 저장되어 있던 값을 순수한 데이터로써 확인할 수 있는 유일한 중간 다리라는 것이다.
이를 활용하면 데이터를 DB에 넣기 이전 암호화를 수행할 수도 있고, 반대로 DB에서 데이터를 빼와서 암호화된 데이터를 복호화시킨 이후 암호화 이전의 Raw Data를 user에게 보여주는 작업을 수행할 수도 있게 되는 것이다.
암호화를 예로 들었지만, DB에 저장할 데이터에 대한 전처리나 DB에서 가지고 온 데이터에 대한 후처리도 가능할 것이다.
위에서 명시한 코드는 데이터를 DB에 저장할 때 암호화를 하며, 반대로 DB에서 데이터를 뽑을 때 암호화된 데이터를 복호화시키는 과정을 추가시키기 위해 Override 하여 구현한 Custom TypeHandler이다. 다른 블로그에서 퍼온 코드이지만 TypeHandler 활용법에 대해 잘 표현하고 있다고 생각해서 가지고 왔다.
TypeHandler는 결국 사용할 Query의 Output이 적절한지를 검증하는 방법이다. Default TypeHandler들은 Default Java Type과 연동되어 Query의 Value가 내가 원하는 Java Type으로 변환 가능한 Value인지를 확인하는 Case가 많다.
아래 기입할 TypeHandler들은 {타입 핸들러} : {해당 핸들러가 매칭 될 Java Type}
쌍으로 기입되어 있다
위에서 보듯이, 결국 DB에서 뽑아온 값이 내가 원하는 Java Type과 연동되는가(변환되는가)를 따지기 위한 방법이라고 이해하면 될 것 같다.
다음 Section에서 배우겠지만 Default Java Type 같은 경우 TypeHandler도 활용하지만 javaType 파라미터를 통해 쉽고 직관적인 설정이 가능하므로 javaType 방법도 자주 활용하는 방식이다
MyBatis는 Query의 결과를 자바 타입, 즉 객체(Instance)로 반환한다. 그렇다면 어떻게 MyBatis는 Query의 결과문을 객체에 담아 반환할 수 있는 걸까?
MyBatis는 결과를 Instance에 담기 위해 ObjectFactory라는 것을 활용한다. ObjectFactory는 Override 해서도 활용할 수 있지만, 이미 구현된 DefaultObjectFactory가 매우 잘 구현되어 있으므로 큰 의미는 없다.
MyBatis에서 Query가 결과문을 내놓으면 ObjectFactory는 Query의 결괏값을 객체 안에 존재하는 멤버 변수에 값을 담는다. MyBatis는 이렇게 만들어진 객체를 반환하고 우리는 ObjectFactory가 만든 객체를 보는 것이므로 MyBatis가 결과를 Instance로 반환하는 것이라고 인지하게 되는 것이다.
DefaultObjectFactory를 Override 하는 것에 큰 의미는 없지만 DefaultObjectFactory의 내부 메서드를 보는 것은 ObjectFactory의 동작 방식에 대한 이해를 돕기 때문에 활용하지는 않더라도 소스 코드를 보는 것을 추천한다
public class ExampleObjectFactory extends DefaultObjectFactory {
@Override
public <T> T create(Class<T> type) {
// Parameter가 없는 기본 생성자
return super.create(type);
}
@Override
public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
// Parameter가 있는 생성자를 다룸
return super.create(type, constructorArgTypes, constructorArgs);
}
@Override
public void setProperties(Properties properties) {
// Object Factory에 대한 Property 적용
super.setProperties(properties);
}
@Override
public <T> boolean isCollection(Class<T> type) {
// 결과값이 Collection인지 아닌지를 반환함
// 아마 isCollection = true일 경우 객체를 List에 담아 반환하게 될 것이다.
return Collection.class.isAssignableFrom(type);
}}
사실 설정 방법은 그렇게까지 중요하다고는 볼 수 없다.
settings 같은 경우 Default로만 설정해도 충분히 작동하며 typeAliases도 우리는 메서드를 통해 SqlSessionfactory에 먹여주므로 큰 걱정할 필요가 없고 objectFactory는 Override 해서 활용할 일이 거의 없기 때문에 활용할 일이 없다.
typeHandler 같은 경우만 컬럼이나 resultMap에 대한 검증 및 DB Query 결과문에 대한 처리나 사전 작업을 위해서 활용할 수는 있을 것이다.
즉, Setting name과 Value를 외우는 것에 집중하기보다는 어떤 기능이 존재하며, 어떤 방식으로 설정하는지 방법에 대해서만 이해할 수 있다면 완벽하게 Setting 방법을 익혔다고 할 수 있을 것 같다.
이제는 MyBatis의 핵심인 SQL Mapping File(XML 파일)에 대해 알아보자