Spring/초기 setting

2) MyBatis 3.0 다중 스키마 설정

쿠카이든 2023. 3. 19. 23:02
728x90
MyBatis 3.0 다중 스키마 설정

아래 내용은 https://github.com/beaver84/setting-test 에서 실제 소스를 확인할 수 있습니다.

JDBC가 작동되는 과정. DAO는 사용자가 직접 만든 코드 (DataAccessObject)

  • 우리가 사용하는 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가 존재한다.

참고로 기본 구현체는 DataSourceTxManager이고, SpringDataJPA 사용시 JpaTxManager로 설정을 바꾼다.


  • 그럼 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패키지를 만든다.

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 파일을 추가한다(다중 스키마 설정).
728x90
@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 위치 및 파일을 수정한다.

 

  • 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

 

728x90