MyBatis 3.0 다중 스키마 설정
아래 내용은 https://github.com/beaver84/setting-test 에서 실제 소스를 확인할 수 있습니다.
- 우리가 사용하는 Spring와 하이버네이트에서 제공해주는 @Transactional는 알아서 트랜잭션을 관리해주는 마법의 키워드가 아니다. 추상화해서 사용할 뿐이지 실제는 위와같이 JDBC 트랜잭션을 사용하여 구현한다.
- DAO에서 Database에 접근하기 위해서는 Spring-jdbc가 필요하고, 이는 DataSource 정보로부터 구할 수 있다.
- 또한, 스프링은 트랜잭션 처리를 TranscationManager 객체를 통해 처리한다.
- 구현체는 갈아끼울 수 있게 인터페이스인 PlatformTranscationManager가 주입되어 사용된다.
public interface PlatformTransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
void commit(TransactionStatus var1) throws TransactionException;
void rollback(TransactionStatus var1) throws TransactionException;
물론 구현체마다 거의 동일한 부분이 있을 수 있어서, 이를 구현하는 AbstarctPlatformTxManager가 존재한다.
- 그럼 1)번에서 생성한 springboot 프로젝트에 MyBatis 설정을 추가한다.
- build.gradle 파일에 아래와 같이 라이브러리들을 추가한다.
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.9'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
//Spring JDBC
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
//Mybatis 관련 라이브러리
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.0'
runtimeOnly 'mysql:mysql-connector-java:8.0.25'
compileOnly 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//Apache에서 제공하는 Database Connection Pool 라이브러리
implementation group: 'org.apache.commons', name: 'commons-dbcp2', version: '2.7.0'
//자바 API의 핵심 클래스의 조작을 편리하게 지원(ArrayUtils 등)
implementation 'org.apache.commons:commons-lang3:3.10'
//스프링에서 SQL문을 실행한 로그를 보기 위한 라이브러리
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
}
- 추가 후에 코끼리 모양 버튼을 눌러 새로고침을 한다.
- 또한, application.properties에서 DB연결 정보를 수정한다.
spring.profiles.active=local
server.port = [port 번호]
teamflace.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
teamflace.datasource.url=jdbc:log4jdbc:mysql://localhost:3306/[스키마이름]?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&autoReconnection=true
teamflace.datasource.username=[ID]
teamflace.datasource.password=[PASSWORD]
teamflace.datasource.initialSize=5
teamflace.datasource.maxWait=6000
teamflace.datasource.maxActive=8
teamflace.datasource.validationQuery=select 1
teamflace.datasource.testOnBorrow=false
teamflace.datasource.testOnReturn=false
teamflace.datasource.testWhileIdle=true
teamflace.datasource.maxIdle=8
teamflace.datasource.minIdle=1
teamflace.datasource.timeBetweenEvictionRunsMillis=50000
teamflace.datasource.mapper-locations=classpath:/mapper/**/*.xml
logging.level.root=debug
logging.level.org.springframework.web=DEBUG
logging.config=classpath:logback-local.xml
그리고 resources 폴더 하위에 sql을 저장할 mapper 폴더를 만든다.
- 이제 자바 파일만 생성하면 셋팅이 끝난다.
- java → 루트 패키지(예 : com.example.settingtest)의 하위에 config 패키지를 만들고 하위에 typeHandler패키지를 만든다.
![](https://blog.kakaocdn.net/dn/pTGEp/btr4yjPfa2n/W5HsaUhvNR98KVBMsdsKw0/img.png)
typeHandler 패키지 안에는 이중 LocalDateTimeTypeHandler.java 파일을 만들고 아래 내용을 작성한다.
@MappedTypes(LocalDateTime.class)
public class LocalDateTimeTypeHandler extends BaseTypeHandler<LocalDateTime> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType)
throws SQLException {
ps.setTimestamp(i, Timestamp.valueOf(parameter));
}
@Override
public LocalDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
Timestamp timestamp = rs.getTimestamp(columnName);
return getLocalDateTime(timestamp);
}
@Override
public LocalDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Timestamp timestamp = rs.getTimestamp(columnIndex);
return getLocalDateTime(timestamp);
}
@Override
public LocalDateTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Timestamp timestamp = cs.getTimestamp(columnIndex);
return getLocalDateTime(timestamp);
}
private static LocalDateTime getLocalDateTime(Timestamp timestamp) {
if (timestamp != null) {
return timestamp.toLocalDateTime();
}
return null;
}
}
typeHandler는 Mybatis일 때, DB에서 꺼낸 데이터가 LocalDateTime에 입력될 때 오류가 발생하는데, 커스텀 클래스를 생성하여 이를 방지하는 방법이다(참고 : https://umbum.dev/922 ).
- 설정파일 내용을 스프링 컨테이너에 주입하기 위해 config 패키지 아래에 MybatisConfig.java 파일을 추가한다.
@Configuration
@Lazy
@EnableTransactionManagement
@EnableAutoConfiguration(exclude = {MybatisAutoConfiguration.class})
public class MybatisConfig {
private Logger log = LogManager.getLogger(this.getClass());
public static final Integer QUERY_TIMEOUT = 10;
//application.properties 파일에 teamflace.datasource로 시작하는 접속 정보를 Properties 객체로 가져온다.
@ConfigurationProperties(prefix = "teamflace.datasource")
@Bean(name = "teamfleshDatasourceProperties")
public Properties teamfleshDatasourceProperties() {
return new Properties();
}
//위에서 만들어진 properties 정보를 바탕으로 spring JDBC에 필요한 dataSource를 생성한다.
@Bean(name = "mybatisDataSource")
public DataSource mybatisDataSource() throws Exception {
Properties properties = teamfleshDatasourceProperties();
log.info("teamfleshDatasourceProperties ===> {}", properties);
BasicDataSource dataSource = BasicDataSourceFactory.createDataSource(properties);
dataSource.setDefaultQueryTimeout(QUERY_TIMEOUT);
return dataSource;
}
//SqlSessionFactory 객체란? -> DataSource를 참조하여 MyBatis와 Mysql 서버를 연동
//위에서 만들어진 datasource를 바탕으로 mybatis session 정보를 담은 SqlSessionFactory 객체를 만든다.
@Bean(name = "mybatisSqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(mybatisDataSource());
sessionFactoryBean.setTypeHandlers(
new LocalDateTimeTypeHandler());
//model entity alias 감지를 위한 패키지 지정
sessionFactoryBean.setTypeAliasesPackage("com.example.settingtest.domain");
Resource[] appMappers = resolver.getResources("mapper/**/*.xml");
Resource[] defaultMappers = resolver.getResources("mapper/*.xml");
Resource[] mappers = ArrayUtils.addAll(appMappers, defaultMappers);
sessionFactoryBean.setMapperLocations(mappers);
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setDefaultStatementTimeout(QUERY_TIMEOUT);
//entity 카멜명 필드와 mybatis 쿼리문의 snake명 필드명을 맵핑
configuration.setMapUnderscoreToCamelCase(true);
//쿼리의 결과 필드가 null 인경우, null 누락되어서 response 에 나갓는것을 방지
configuration.setCallSettersOnNulls(true);
//쿼리에 보내는 파라메터가 null인 경우, 오류 발생하는 것 방지(설정 필요한지 고려 필요)
configuration.setJdbcTypeForNull(null);
sessionFactoryBean.setConfiguration(configuration);
log.info("sessionFactoryBean ===> {}", sessionFactoryBean.toString());
return sessionFactoryBean.getObject();
}
//JPA와 같이 사용하기 위해 작업을 한 트랜잭션으로 묶기 위한 배경작업
@Bean(name = "mybatisTransactionManager")
public PlatformTransactionManager mybatisTransactionManager() throws Exception {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(mybatisDataSource());
log.info("transactionManager ===> {}", transactionManager.toString());
transactionManager.setGlobalRollbackOnParticipationFailure(false);
return transactionManager;
}
}
- @Lazy : @Component와 @Bean에 @Lazy 어노테이션이 정의(value=true)되었다면 다른 참조되는 빈에 의해 사용되거나 실제 참조될 때 로드된다.(LAZY Loading, 지연로딩)
- @Configuration 와 @Bean
- 기본 설정 파일의 경우, @Configuration 하위에 @Bean 들을 추가하여 서버가 기동 시 해당 메서드들을 스프링 컨테이너에 등록한다.
- 어플리케이션 전역적으로 사용되는 클래스(주로 설정파일)의 경우, 싱글톤을 보장하기 위해서도 이 방법을 많이 사용한다.
- @EnableTransactionManagement
- 두개 이상의 쿼리를 하나의 작업으로 묶어야 할 필요가 있을 때, 하나라도 실패하면 전체를 실패로 간주하고 이전에 실행한 쿼리를 취소하기 위해 사용한다.
- 작업을 트랜잭션(@Transactional) 단위로 개발에 적용하기 위한 기반 작업이다.
- 실질적으로 Mysql 서버와 MyBatis를 연결해주는건 SqlSessionFactory라는 객체이다.
- 마지막으로 JPA와 한 트랜잭션으로 묶기 위한 config 패키지 하위에 RdbConnectionConfiguration.java 파일을 추가한다(다중 스키마 설정).
@Configuration
@Lazy
@EnableTransactionManagement
public class RdbConnectionConfiguration {
@Bean(name = "transactionManager")
@Primary
public PlatformTransactionManager transactionManager(
@Qualifier("mybatisTransactionManager") PlatformTransactionManager mybatisTransactionManager) {
return new ChainedTransactionManager(
mybatisTransactionManager
);
}
}
- Qualifier 어노테이션
- 사용할 의존 객체를 선택할 수 있도록 해준다.
- Mybatis외 JPA를 함께 설정하여 하나의 PlatformTransactionManager로 묶었을 때, 어느 Transaction의 DB접속정보를 사용할지를 구분한다.
- 마지막으로 중복된 코드를 줄이기 위해 repository 폴더 아래에 다음 abstract class를 추가한다.
public abstract class AbstractMybatisRepository {
private Logger log = LogManager.getLogger(this.getClass());
public SqlSessionFactory sqlSessionFactory;
public String mapperPrefix;
protected void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
throw new NotImplementedException();
}
protected void setMapperPrefix() {
throw new NotImplementedException();
}
public int insert(String statement, Map<String, Object> params) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
int result = session.insert(mapperPrefix + statement, params);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public int insert(String statement, Object params) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
int result = session.insert(mapperPrefix + statement, params);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public int update(String statement, Map<String, Object> params) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
int result = session.update(mapperPrefix + statement, params);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public int update(String statement, Object params) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
int result = session.update(mapperPrefix + statement, params);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public <T> T selectOne(String statement, Map<String, Object> params) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
T result = session.selectOne(mapperPrefix + statement, params);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public <T> T selectOne(String statement, Object params) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
T result = session.selectOne(mapperPrefix + statement, params);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public <E> List<E> selectList(String statement, Map<String, Object> parameter) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
List<E> result = session.selectList(mapperPrefix + statement, parameter);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public <T> T selectOne(String statement) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
T result = session.selectOne(mapperPrefix + statement);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public <E> List<E> selectList(String statement) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
List<E> result = session.selectList(mapperPrefix + statement);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public <K, V> Map<K, V> selectMap(String statement, String mapKey) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
Map<K, V> result = session.selectMap(mapperPrefix + statement, mapKey);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public <K, V> Map<K, V> selectMap(String statement, Map<String, Object> parameter, String mapKey) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
Map<K, V> result = session.selectMap(mapperPrefix + statement, parameter, mapKey);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public int insert(String statement) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
int result = session.insert(mapperPrefix + statement);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public int update(String statement) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
int result = session.update(mapperPrefix + statement);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
// log.error("{}", e.getStackTrace());
session.close();
throw e;
}
}
public int delete(String statement) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
int result = session.delete(mapperPrefix + statement);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public int delete(String statement, Map<String, Object> params) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
int result = session.delete(mapperPrefix + statement, params);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public int delete(String statement, Object params) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
int result = session.delete(mapperPrefix + statement, params);
session.close();
return result;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public <T> T insertLastSelectId(String statement, Map<String, Object> params) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
session.insert(mapperPrefix + statement, params);
T lastSelectedId = session.selectOne("com.marvrus.moon_app_api.repository.mapper.defaultMapper.selectLastInsertId");
session.close();
return lastSelectedId;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
public <T> T insertLastSelectId(String statement) {
SqlSession session = this.sqlSessionFactory.openSession();
try {
session.insert(mapperPrefix + statement);
T lastSelectedId = session.selectOne("com.marvrus.moon_app_api.repository.mapper.defaultMapper.selectLastInsertId");
session.close();
return lastSelectedId;
} catch (Exception e) {
log.error("DB execution error : {}", e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
log.error("{}", stackTraceElement.toString());
}
session.close();
throw e;
}
}
}
- Mybatis 테스트 시작(테스트용 관련 member의 domain, repository 추가한다)
- 테스트용 member 조회 관련 mapper.xml 위치 및 파일을 수정한다.
![](https://blog.kakaocdn.net/dn/cEXn9f/btr4AUVX6JP/eyhz35K8oHSOdASss1MUVK/img.png)
- MySql DB에 테스트용 테이블 및 데이터 추가
create table if not exists teamflace.member
(
id int auto_increment primary key,
email varchar(50) null,
name varchar(20) null,
address varchar(100) null,
password varchar(50) null
);
insert into teamflace.member (email, name, address, password)
values ('eden@timf.co.kr', '배이든', '구로주공아파트', '1234');
- 테스트 코드 작성
package com.example.settingtest.repository;
(임포트 구문 생략)
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Transactional
class MemberMybatisRepositoryImplTest {
@Autowired
MemberMybatisRepository memberMybatisRepository;
@Test
@DisplayName("멤버 조회 Mybatis 테스트")
void findByName() {
//given & when
List<Member> allUser = memberMybatisRepository.findAllUser();
//then
assertThat(allUser).extracting(Member::getName).containsAnyOf("배아무개");
assertThat(allUser).extracting(Member::getAddress).containsAnyOf("한국아파트");
assertThat(allUser).extracting(Member::getEmail).containsAnyOf("baekk@timf.co.kr");
}
}
- 테스트 통과 확인
참고 : https://jiwondev.tistory.com/154
@Transactional 의 동작원리, 트랜잭션 매니저
💭 JDBC에서 사용하는 트랜잭션 자바의 JDBC에서 개발자가 직접 트랜잭션을 관리하는 방법은 한가지 밖에 없습니다. // 커넥션 풀에서 DB커넥션을 받아왔다고 가정 Connection connection = dataSource.getConn
jiwondev.tistory.com
'Spring > 초기 setting' 카테고리의 다른 글
6) 스프링 시큐리티(spring-security) 적용 (0) | 2023.04.05 |
---|---|
5) 스프링 시큐리티(spring-security) 개요 (0) | 2023.04.05 |
4) Querydsl 설정 (2) | 2023.03.20 |
3) JPA 다중 스키마 설정 (0) | 2023.03.19 |
1) 스프링 부트 프로젝트 생성 (0) | 2023.03.19 |