💬
목차
< 뒤로가기
인쇄

캐모마일 데이터베이스 모듈 가이드

개요

스프링 프레임워크를 사용하여 웹 개발을 진행함에 있어서 매 프로젝트마다 새롭게 데이터베이스 관련 설정을 진행 하는 것은 복잡한 일이다. 캐모마일 데이터베이스 모듈은 데이터베이스 관련 작업을 진행함에 있어서 유용한 모듈을 제공하여 쉽고 빠르게 프로젝트 개발을 시작 할 수 있도록 지원한다.

기능 설명

마이바티스 배치

일반적으로 마이바티스에서 배치 쿼리를 작성할 때 다음과 같은 방법으로 해결한다.

<foreach collection="list" item="student" separator=";">
INSERT INTO TB_USER (name, id, password)
VALUES (#{student.name}, #{student.id}, #{student.password})
</foreach>

그러나 다음과 같은 방법은 아래와 같은 방법으로 쿼리가 동작하기 때문에 대량의 insert가 발생할 경우 성능 저하 현상이 발생할 수 있다.

INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');
INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');
INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');
INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');
INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');
INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');
INSERT INTO TB_USER(name, id, password) VALUES ('abc', 'abc', '1234');

따라서 캐모마일 에서는 마이바티스 환경에서 배치 쿼리 작성을 손쉽게 진행할 수 있도록 다음과 같이 마이바티스 플러그인을 제공한다.

올바르게 캐모마일 샘플 프로젝트를 실행했다면 다음과 같은 파일이 있을 것이다.

resources/sql/config/sqlMapConfig.xml

<?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="true"/>  
    <setting name="jdbcTypeForNull" value="NULL"/>  
    <setting name="callSettersOnNulls" value="true"/>  
  </settings>  <plugins>    <plugin interceptor="net.lotte.chamomile.module.database.mybatis.MapperLogInterceptor"/>  
    <plugin interceptor="net.lotte.chamomile.module.database.mybatis.pageable.PageableInterceptor"/>  
    <plugin interceptor="net.lotte.chamomile.module.database.mybatis.batch.BatchableInterceptor"/>  
  </plugins>  
</configuration>

여기서 BatchableInterceptor 가 플러그인으로 등록되어 있는 것을 볼 수 있다. 다음과 같이 설정이 완료 되었다면 사용 방법은 간단하다.

void insertResourceExcel(List<ResourceExcelVO> command, BatchRequest batchRequest);

다음과 같이 mapper에서 인터페이스를 작성해주고,

@Override  
public void createResource(List<ResourceExcelVO> command) {  
    resourceMapper.insertResourceExcel(command, new BatchRequest(1000));  
}

서비스에서 다음과 같이 list 형태의 객체와 배치 사이즈를 설정해주면 해당 사이즈대로 배치 쿼리가 동작하게 된다.

<!-- 리소스 저장 -->  
<insert id="insertResourceExcel" parameterType="net.lotte.chamomile.admin.resource.domain.ResourceExcelVO">  
  <![CDATA[  
  INSERT INTO CHMM_RESOURCE_INFO  
  (RESOURCE_ID  
  ,RESOURCE_URI  
  ,RESOURCE_NAME  
  ,RESOURCE_DESC  
  ,RESOURCE_HTTPMETHOD  
  ,SECURITY_ORDER  
  ,USE_YN  
  ,SYS_INSERT_DTM  
  ,SYS_INSERT_USER_ID  
  ,SYS_UPDATE_DTM  
  ,SYS_UPDATE_USER_ID)
    VALUES    
  (#{resourceId}    
  ,#{resourceUri}    
  ,#{resourceName}    
  ,#{resourceDesc}    
  ,#{resourceHttpMethod}    
  ,#{securityOrder}    
  ,#{useYn}    
  ,#{sysInsertDtm}    
  ,#{sysInsertUserId}    
  ,#{sysUpdateDtm}    
  ,#{sysUpdateUserId})  
  ]]>  
  </insert>

다음과 같이 배치 삭제도 가능하다.

void deleteGroup(List<String> groupId, BatchRequest batchRequest);
groupMapper.deleteGroup(deleteIds, new BatchRequest(1000));
<!-- 그룹 삭제 -->  
<delete id="deleteGroup" parameterType="java.util.List">  
  DELETE FROM  
    CHMM_GROUP_INFO  
    <where>  
    GROUP_ID = #{groupId}  
    </where>  
</delete>

마이바티스 Pageable

마이바티스를 사용하여 쿼리를 작성하다보면 다음과 같이 sort와 페이징을 위해 동적으로 쿼리를 작성한다.

<!-- 유저 List 조회 -->  
<select id="groupListData" parameterType="net.lotte.chamomile.admin.group.vo.GroupVO"  
    resultType="net.lotte.chamomile.admin.group.vo.GroupVO">  

<![CDATA[  
    SELECT
        GROUP_ID groupId, /* 그룹_아이디 */
        GROUP_NAME groupName, /* 그룹_명 */
        GROUP_DESC groupDesc, /* 그룹_설명 */
        USE_YN useYn, /* 사용 여부 */
        SYS_INSERT_DTM sysInsertDtm, /* 시스템_입력_일시 */
        SYS_INSERT_USER_ID sysInsertUserId, /* 시스템_입력_사용자_아이디 */
        SYS_UPDATE_DTM sysUpdateDtm, /* 시스템_수정_일시 */
        SYS_UPDATE_USER_ID sysUpdateUserId /* 시스템_수정_사용자_아이디 */
     FROM   
       CHMM_GROUP_INFO A  
    ]]>  
    <!-- 검색조건 -->  
    <where>  
       <if test="searchGroupId != null and searchGroupId != ''"> AND GROUP_ID LIKE CONCAT(CONCAT('%',#{searchGroupId}),'%')  </if>  
       <if test="searchGroupName != null and searchGroupName != ''"> AND GROUP_NAME LIKE CONCAT(CONCAT('%',#{searchGroupName}),'%')  </if>  
       <if test="searchUseYn != null and searchUseYn != ''"> AND USE_YN = #{searchUseYn}  </if>  
    </where>  

    ORDER BY   
<choose>  
          <when test="orderCol != null and orderCol != ''">  
             <choose>  
                <when test="orderCol eq 'groupId'"> GROUP_ID </when>  
                <when test="orderCol eq 'groupName'"> GROUP_NAME </when>  
                <otherwise> GROUP_ID </otherwise>  
             </choose>  
             <choose>  
                <when test="sortAsc eq 'true'"> ASC, </when>  
                <otherwise> DESC, </otherwise>  
             </choose>  
          </when>  
       </choose>  
</select>

다음과 같이 코드를 작성하면 SQL이 비즈니스의 핵심 로직과는 동떨어진다. 또한 핵심 SQL 호출보다 페이징과 정렬이 더 긴 쿼리를 작성하게 되는 일이 발생한다.

그래서 캐모마일은 Spring Data의 Pageable과 PageableInterceptor를 사용해서 쿼리 작성을 돕는다.

사용 방법은 다음과 같다.

  1. 컨트롤러 영역에서 Spring Pageable로 파라미터를 받는다.

화면에서는 다음과 같이 입력하면 Pageable에 인자로 값이 넘어가게 된다.

{
  "page": "0",
  "size": "10",
  "sort": "groupId,desc"
}
@GetMapping(path = "/list")  
public ChamomileResponse<Page<ResourceVO>> getResourceList(ResourceQuery request, Pageable pageable) {  
    Page<ResourceVO> results = resourceService.getResourceList(request, pageable);  
    return new ChamomileResponse<>(results);  
}
  1. Mapper에서 인터페이스로 Pageable을 파라미터로 전달한다.
Page<ResourceVO> findResourceListData(ResourceQuery query, Pageable pageable);
  1. 쿼리를 작성한다.
<!-- 리소스 목록 조회 -->  
<select id="findResourceListData" parameterType="net.lotte.chamomile.admin.resource.api.dto.ResourceQuery"  
        resultType="net.lotte.chamomile.admin.resource.domain.ResourceVO">  

  SELECT  
  RESOURCE_ID resourceId, /* 리소스 아이디 */  
  RESOURCE_URI resourceUri, /* 리소스 경로 */  
  RESOURCE_NAME resourceName, /* 리소스 명 */  
  RESOURCE_HTTPMETHOD resourceHttpMethod, /* http method */  RESOURCE_DESC resourceDesc, /* 리소스_설명 */  
  SECURITY_ORDER securityOrder, /* 리소스 순서 */  
  USE_YN useYn, /* 사용 여부 */  
  SYS_INSERT_DTM sysInsertDtm, /* 시스템_입력_일시 */  
  SYS_INSERT_USER_ID sysInsertUserId, /* 시스템_입력_사용자_아이디 */  
  SYS_UPDATE_DTM sysUpdateDtm, /* 시스템_수정_일시 */  
  SYS_UPDATE_USER_ID sysUpdateUserId /* 시스템_수정_사용자_아이디 */  
  FROM  CHMM_RESOURCE_INFO A  <!-- 검색조건 -->  
  <where>  
    <if test="searchResourceId != null and searchResourceId != ''">AND RESOURCE_ID LIKE CONCAT(CONCAT('%',#{searchResourceId}),'%')</if>  
    <if test="searchResourceName != null and searchResourceName != ''">AND RESOURCE_NAME LIKE CONCAT(CONCAT('%',#{searchResourceName}),'%')</if>  
    <if test="searchUseYn != null and searchUseYn != ''">AND USE_YN = #{searchUseYn}</if>  
  </where>  

</select>

이렇게 캐모마일에서 제공하는 PageableInterceptor를 잘 사용한다면 비즈니스 로직에 집중하는 쿼리를 작성할 수 있다.

마이바티스 변경 감지

서비스를 운영하는 도중에 서버를 내리지 않고 마이바티스의 쿼리를 변경하고 싶을 때가 있다. 해당 상황에서 유용하게 사용하기 위해 캐모마일에서는 RefreshableSqlSessionFactoryBean을 제공한다.

application.yml

chmm:
    mapper:  
        interval: 60000  
        refreshable: true  //해당옵션으로 리프레쉬 적용 가능
        config-location: classpath:/sql/config/sqlMapConfig.xml  
        refreshable-mapper-locations: classpath:/sql/service/*.xml //해당 옵션으로 위치 지정
        mapper-locations: sql/service/*.xml

트랜잭션 관리

데이터베이스를 사용 할 때 JPA 와 MyBatis를 동시에 사용한다면 두 개의 데이터베이스 호출 방식의 차이 때문에 두 개의 트랜잭션을 하나로 통합해주는 과정이 필요하다. 캐모마일을 사용한다면 해당 설정을 자동으로 구성해준다. 또한 pointcut을 사용하여 전역으로 트랜잭션을 관리할 수 있다.

application.yml

chmm:
    txPointcut:  
        expression: (execution(* net.lotte.chamomile.admin..*ServiceImpl.*(..)))

위와 같이 전역으로 트랜잭션을 묶게 된다면 클래스에 @Transactional을 사용하지 않고, 개발자 각자의 로직에 따른 트랜잭션 관리가 아닌, 공통 개발자 한 명에 의해서 전체 프로젝트의 트랜잭션을 관리할 수 있다.

데이터베이스 Audit

데이터베이스 테이블을 구성할 때 생성자, 생성일시, 수정자, 수정일시 등의 데이터가 필요한 경우가 많다. 캐모마일에서는 해당 모듈에서 필요한 객체를 상속 받아 쉽게 create/update 관리를 진행할 수 있다.

TimeAuthorLog.java

package net.lotte.chamomile.module.database.audit;  

import java.time.LocalDateTime;  

import javax.persistence.Column;  
import javax.persistence.EntityListeners;  
import javax.persistence.MappedSuperclass;  
import javax.persistence.PrePersist;  
import javax.persistence.PreUpdate;  

import lombok.AllArgsConstructor;  
import lombok.Getter;  
import lombok.NoArgsConstructor;  
import lombok.Setter;  
import lombok.experimental.SuperBuilder;  

import org.springframework.data.annotation.CreatedBy;  
import org.springframework.data.annotation.LastModifiedBy;  
import org.springframework.data.jpa.domain.support.AuditingEntityListener;  
import org.springframework.security.core.Authentication;  
import org.springframework.security.core.context.SecurityContext;  
import org.springframework.security.core.context.SecurityContextHolder;  

@MappedSuperclass  
@EntityListeners(AuditingEntityListener.class)  
@SuperBuilder  
@NoArgsConstructor  
@AllArgsConstructor  
@Getter  
@Setter  
public class TimeAuthorLog {  
    @Column(name = "SYS_INSERT_DTM", updatable = false)  
    protected LocalDateTime sysInsertDtm;  

    @Column(name = "SYS_UPDATE_DTM")  
    protected LocalDateTime sysUpdateDtm;  

    @CreatedBy  
    @Column(name = "SYS_INSERT_USER_ID", updatable = false)  
    protected String sysInsertUserId;  

    @LastModifiedBy  
    @Column(name = "SYS_UPDATE_USER_ID")  
    protected String sysUpdateUserId;  

    @PrePersist  
    public void onCreate() {  
        LocalDateTime now = LocalDateTime.now();  
        sysInsertDtm = now;  
        sysUpdateDtm = now;  
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();  
        if(authentication != null) {  
            sysInsertUserId = authentication.getName();  
            sysUpdateUserId = authentication.getName();  
        }    }  
    @PreUpdate  
    public void onUpdate() {  
        sysUpdateDtm = LocalDateTime.now();  
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();  
        if(authentication != null) {  
            sysUpdateUserId = authentication.getName();  
        }    }}

ResourceVO.java – 예시

@AllArgsConstructor  
@NoArgsConstructor  
@SuperBuilder  
@Getter  
@Setter  
public class ResourceVO extends TimeAuthorLog {  
    private String resourceId;  
    private String resourceDesc;  
    private String resourceHttpMethod;  
    private String resourceName;  
    private String resourceUri;  
    @NotNull(message = "숫자를 입력하세요.")  
    @Min(value = 1, message = "리소스 순서는 1~5 자리의 양수만 입력 가능합니다.")  
    @Max(value = 99999, message = "리소스 순서는 1~5 자리의 양수만 입력 가능합니다.")  
    private Integer securityOrder;  
    @Pattern(regexp = "^(0|1)$", message = "참/거짓은 0/1로 구분 합니다.")  
    private String useYn;  

    private String flag;  

    public ResourceVO(String resourceId) {  
        this.resourceId = resourceId;  
    }
}

ResourceServiceImpl.java – 예시

@Override  
public void updateResource(ResourceVO command) {  
    command.onUpdate();  
    resourceMapper.updateResource(command);  
}

Mybatis환경에서는 위와 같이 메소드를 직접 호출해주면 된다.

JPA환경에서는 EntityManager에서 commit이 발생 한 경우에 onCreate/onUpdate가 자동으로 호출 된다.

데이터베이스 설정

캐모마일의 데이터베이스 설정은 다음과 같이 진행할 수 있다. 캐모마일의 DBCP는 스프링부트 기본 옵션인 Hikari를 사용한다. 스프링을 사용하는 것과 비슷하게 다음과 같이 작성하면 캐모마일 데이터베이스 설정이 끝난다.

application.yml

chmm:
    jdbc:  
        driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy  
        jdbcUrl: jdbc:log4jdbc:mysql://localhost:3306/chamomile?autoReconnect=true&serverTimezone=UTC&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull  
        username: root  
        password: ldcc!2626  
        maximumPoolSize: 10  
        connectionTimeout: 30000  
        maxLifetime: 1800000

다중 데이터베이스 설정

개요

가볍게 운영하는 서비스라면 모르겠지만 사용자가 다량인 경우 부하를 분산하기 위해서, 데이터의 Read & Write를 분리하여 사용하게 된다.
해당 상황에서 하나의 datasource를 사용할 수 없고 각 각 다른 datasource를 사용해야 하는데 이 글은 그와 같은 상황에서의 예제를 다룬다.

예제

  1. 데이터소스 추가 설정
@Configuration
@EnableConfigurationProperties(ChamomileJdbcProperties.class)
@MapperScan(basePackages = "demo.board.domain" ,sqlSessionFactoryRef="customSqlSessionFactory")
@RequiredArgsConstructor
public class CustomDatabaseConfiguration {

    private final ChamomileJdbcProperties jdbcProperties;
    private final ChamomileMapperProperties chamomileMapperProperties;

    @Bean
    @Primary
    public DataSource customDataSource() {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName(jdbcProperties.getDriverClassName());
        hikariConfig.setJdbcUrl(jdbcProperties.getJdbcUrl());
        hikariConfig.setUsername(jdbcProperties.getUsername());
        hikariConfig.setPassword(jdbcProperties.getPassword());
        hikariConfig.setMaximumPoolSize(jdbcProperties.getMaximumPoolSize());
        hikariConfig.setConnectionTimeout(jdbcProperties.getConnectionTimeout());
        hikariConfig.setMaxLifetime(jdbcProperties.getMaxLifeTime());
        return new HikariDataSource(hikariConfig);
    }

    @Bean
    @Primary
    public RefreshableSqlSessionFactoryBean customSqlSessionFactory(@Qualifier(value = "customDataSource") final DataSource dataSource,
                                                               final VendorDatabaseIdProvider databaseIdProvider) {
        RefreshableSqlSessionFactoryBean refreshableSqlSessionFactoryBean = new RefreshableSqlSessionFactoryBean() {{
            setDataSource(dataSource);
            setConfigLocation(chamomileMapperProperties.getConfigLocation());
            setRefreshable(chamomileMapperProperties.isRefreshable());
            setRefreshableMapperLocations(chamomileMapperProperties.getRefreshableMapperLocations());
            setDatabaseIdProvider(databaseIdProvider);
            setInterval(chamomileMapperProperties.getInterval());
        }};
        chamomileMapperProperties.setMapperLocations(new String[]{"classpath:/sql/service/*.xml"});
        refreshableSqlSessionFactoryBean.setMapperLocations(chamomileMapperProperties.resolveMapperLocations());
        return refreshableSqlSessionFactoryBean;
    }

    @Bean
    public SqlSessionTemplate customSqlSession(@Qualifier(value = "customSqlSessionFactory")SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean
    public PlatformTransactionManager customTxManager1(@Qualifier(value = "customDataSource")final DataSource dataSource) {
        return new DataSourceTransactionManager() {{
            setDataSource(dataSource);
        }};
    }
}

@Configuration
@EnableConfigurationProperties(ChamomileJdbc2Properties.class)
@MapperScan(basePackages = "demo.board.domain2" ,sqlSessionFactoryRef="customSqlSessionFactory2")
@RequiredArgsConstructor
public class CustomDatabaseConfiguration2 {

    private final ChamomileJdbc2Properties jdbc2Properties;
    private final ChamomileMapperProperties chamomileMapperProperties;

    @Bean
    public DataSource customDataSource2() {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName(jdbc2Properties.getDriverClassName());
        hikariConfig.setJdbcUrl(jdbc2Properties.getJdbcUrl());
        hikariConfig.setUsername(jdbc2Properties.getUsername());
        hikariConfig.setPassword(jdbc2Properties.getPassword());
        hikariConfig.setMaximumPoolSize(jdbc2Properties.getMaximumPoolSize());
        hikariConfig.setConnectionTimeout(jdbc2Properties.getConnectionTimeout());
        hikariConfig.setMaxLifetime(jdbc2Properties.getMaxLifeTime());
        return new HikariDataSource(hikariConfig);
    }

    @Bean
    public RefreshableSqlSessionFactoryBean customSqlSessionFactory2(@Qualifier(value = "customDataSource2") final DataSource dataSource,
                                                                     final VendorDatabaseIdProvider databaseIdProvider) {
        RefreshableSqlSessionFactoryBean refreshableSqlSessionFactoryBean = new RefreshableSqlSessionFactoryBean() {{
            setDataSource(dataSource);
            setConfigLocation(chamomileMapperProperties.getConfigLocation());
            setRefreshable(chamomileMapperProperties.isRefreshable());
            setRefreshableMapperLocations(chamomileMapperProperties.getRefreshableMapperLocations());
            setDatabaseIdProvider(databaseIdProvider);
            setInterval(chamomileMapperProperties.getInterval());
        }};
        chamomileMapperProperties.setMapperLocations(new String[]{"classpath:/sql/service2/*.xml"});
        refreshableSqlSessionFactoryBean.setMapperLocations(chamomileMapperProperties.resolveMapperLocations());
        return refreshableSqlSessionFactoryBean;
    }

    @Bean
    public SqlSessionTemplate customSqlSession2(@Qualifier(value = "customSqlSessionFactory2")SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean
    public PlatformTransactionManager customTxManager2(@Qualifier(value = "customDataSource2")final DataSource dataSource) {
        return new DataSourceTransactionManager() {{
            setDataSource(dataSource);
        }};
    }
}

@Getter
@Setter
@ConfigurationProperties(prefix = "chmm.jdbc")
public class ChamomileJdbcProperties {
    /**
     * jdbc driver class name 입력값.
     */
    private String driverClassName;

    /**
     * 캐모마일 jdbc-url
     */
    private String jdbcUrl;

    /**
     * 데이터베이스 계정 이름
     */
    private String username;

    /**
     * 데이터베이스 계정 비밀번호
     */
    private String password;

    /**
     * Hikari Pool size
     */

    private int maximumPoolSize;

    /**
     * 애플리케이션에서 새로운 데이터베이스 연결을 얻으려고 시도할 때 대기 할 최대 시간.
     */
    private int connectionTimeout;
    /**
     * 데이터베이스 연결이 유지될 수 있는 최대 시간
     */
    private int maxLifeTime;
}

@Getter
@Setter
@ConfigurationProperties(prefix = "chmm.jdbc2")
public class ChamomileJdbc2Properties {
    /**
     * jdbc driver class name 입력값.
     */
    private String driverClassName;

    /**
     * 캐모마일 jdbc-url
     */
    private String jdbcUrl;

    /**
     * 데이터베이스 계정 이름
     */
    private String username;

    /**
     * 데이터베이스 계정 비밀번호
     */
    private String password;

    /**
     * Hikari Pool size
     */

    private int maximumPoolSize;

    /**
     * 애플리케이션에서 새로운 데이터베이스 연결을 얻으려고 시도할 때 대기 할 최대 시간.
     */
    private int connectionTimeout;
    /**
     * 데이터베이스 연결이 유지될 수 있는 최대 시간
     */
    private int maxLifeTime;
}

다음과 같이 설정하면
첫 번째 datasource는 demo.board.domain 패키지의 매퍼를 인식하고,
두 번째 datasource는 demo.board.domain2 패키지의 매퍼를 인식한다.

각각 설정한 xml location에 쿼리를 작성하고 해당 패키지에 mapper를 구현하면 된다.

  1. yaml 설정
chmm:
  jdbc:
    driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
    jdbcUrl: jdbc:log4jdbc:mysql://localhost:3306/chamomile?autoReconnect=true&serverTimezone=UTC&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull
    username: root
    password: 12345678
    maximumPoolSize: 10
    connectionTimeout: 30000
    maxLifetime: 1800000
  jdbc2:
    driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
    jdbcUrl: jdbc:log4jdbc:mysql://localhost:3306/chamomile2?autoReconnect=true&serverTimezone=UTC&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull
    username: root
    password: 12345678
    maximumPoolSize: 10
    connectionTimeout: 30000
    maxLifetime: 1800000

다음과 같이 세팅하면 두 개의 datasource를 하나의 was에서 동시에 사용할 수 있다.

  1. Multi Transaction 설정

위와같이 두 개의 datasource를 설정하면 각각의 datasource가 각각 다른 PlatformTransactionManager에 의하여 트랜잭션이 분리된다.
예를 들어 두개의 트랜잭션이 하나로 묶여야 하는데 앞쪽의 비즈니스 로직이 성공했고 뒤쪽의 비즈니스 로직이 실패했다면 앞쪽의 비즈니스 로직이 롤백되지 않는다.
때문에 이 두개의 트랜잭션을 하나로 묶어주어야한다.

@Configuration
@EnableTransactionManagement
public class GlobalTransaction {

    @Bean
    @Primary
    @Autowired
    public PlatformTransactionManager transactionManager(
            @Qualifier("customTxManager1") PlatformTransactionManager txManager1,
            @Qualifier("customTxManager2") PlatformTransactionManager txManager2) {
        return new ChainedTransactionManager(txManager1, txManager2);
    }
}

다음과 같이 두 개의 트랜잭션을 하나로 묶어주면 쉽게 트랜잭션을 하나로 묶어 관리할 수 있다.

예제 코드

캐모마일에서는 샘플코드로 해당 상황에 따른 코드샘플을 제공한다.
chamomile-sample-boot-security-multi-datasource 아키타입을 메이븐 프로젝트로 실행하면 예제 코드를 실행해볼 수 있다.

다음 캐시