본문 바로가기

Dev/Spring

[Spring] Spring Boot 시작하기 (7) - MyBatis에서의 DataBase 다중 연결

 

 

 

포스팅 시리즈

 

 

 

이번 포스팅에서는 Spring Boot에서 여러 개의 데이터베이스를 연결하는 방법에 대해 알아보겠습니다.

개인적으로 Spring Framework로 개발을 진행할 때 가장 스트레스로 다가오는 부분은 환경 설정인데요, 건드려야 할 부분도 많아 복잡하고 작업도 섬세하게 진행해야 해서 시간이 상당히 오래 걸리기 때문입니다.

그리고 한 번 설정 해 두면 웬만해서는 건드릴 부분이 많지 않기 때문에 금방 잊어버려서 새로운 프로젝트를 할 때마다 헤매는 것을 반복한 경험을 겪은 사람은 저뿐만은 아닐 것입니다.

 

설정과 관련한 부분을 최대한 간소화하고 자동화시킨 Spring Boot에서도 본 주제에 대한 작업은 꽤나 복잡한 편에 속하므로, 포스팅에서 작성하는 코드와 설정을 주의 깊게 봐주시길 바랍니다.

 

본 포스팅에서의 예제는 log4jdbc로 DB 로깅이 세팅되어있는 상태에서 진행되었으므로, logback 로깅 환경이 구축되어있지 않은 경우 예시에서의 application.properties 설정 구문이 약간 다르므로 참고해 주세요.

만약 본 예제와 환경을 맞추고 싶으신 분들은 아래의 링크를 참고해 주세요.

2020/10/06 - [Dev/Spring] - [Spring] Spring Boot 시작하기 (5) - log4jdbc를 이용한 Query로깅

 

[Spring] Spring Boot 시작하기 (5) - log4jdbc를 이용한 Query로깅

이번에는 저번 포스팅 끝에서 언급한 DB 요청과 응답에 대한 로깅 처리에 대해 다루겠습니다. 1. 의존성 주입 build.gradle 파일을 열고, DB 로그 기능을 사용하기 위해 log4jdbc 의존성을 등록합니다. #

dev-overload.tistory.com

 

 

본 포스팅을 정주행 하시는 분들을 위한 당부

더보기

만약, 본 포스팅 시리즈를 정주행 하신 분들은 프로젝트에 SalaryService.java, SalaryMapper.java, SalaryMapper.xml 파일이 존재할 것입니다.

본 포스팅에서는 필요가 없을뿐더러 오히려 구축에 방해가 되므로 위 세 파일들을 삭제해 주시길 바랍니다.

또한 application.properties에 spring.datasource~ 로 시작하는 설정과 mybatis.~ 로 시작하는 설정을 모두 지우거나 주석 처리해 주시길 바랍니다.

 

1. DB 세팅

프로젝트 설정에 앞서, 연동할 데이터베이스를 먼저 설정해 주도록 하겠습니다.

본 예제에서는 데이터베이스를 2개를 사용합니다.

보통 하나 이상의 DB를 연결할 때 각 DB의 구분을 1개의 Master DB, n개의 Slave DB로 구분합니다.

 

아래의 표는 이번 예제에서 사용한 DB 정보입니다.

No. Master/slave Database System Database name IP Port
1 master MySQL MASTER 192.168.0.10 3306
2 slave mariadb SLAVE 192.168.0.13 3306

 

마스터로 사용할 데이터베이스는 윈도에 설치한 Mysql이며, 슬레이브 데이터베이스는 제가 사용하는 라즈베리파이에 설치된 mariadb입니다.

두 데이터베이스 모두 기본 포트를 사용했으며, 생성한 데이터베이스도 알기 쉽게 MASTER, SLAVE로 생성했습니다.

 

각 데이터베이스는 외부에서 접속할 수 있는 환경을 마련해 두어야 합니다.

DB 외부 사용에 대한 정보는 아래 링크를 참고해 주세요.

zetawiki.com/wiki/MySQL_%EC%9B%90%EA%B2%A9_%EC%A0%91%EC%86%8D_%ED%97%88%EC%9A%A9

 

MySQL 원격 접속 허용 - 제타위키

다음 문자열 포함...

zetawiki.com

 

 

마스터와 슬레이브 DB에 아래의 쿼리문을 사용해 테이블 생성 및 테스트 데이터를 세팅해 주겠습니다.

구분의 용이를 위해 마스터 DB의 테이블은 SALARY, 슬레이브 DB는 COUNTRY로 종류를 구분해 주었습니다.

 

Master DataBase

# Master DataBase
mysql> CREATE DATABASE MASTER;
mysql> USE MASTER;
mysql> CREATE TABLE SALARY(ID INT NOT NULL PRIMARY KEY, NAME CHAR(10), EMAIL CHAR(20) NOT NULL);

mysql> INSERT INTO SALARY VALUES (1,'AAA','AAA@example.com');
mysql> INSERT INTO SALARY VALUES (2,'BBB','BBB@example.com');
mysql> FLUSH PRIVILEGES;

 

Master DB

위 사진과 같이 테이블이 세팅되었는지 확인합니다.

 

Slave DataBase

# Slave DataBase
mysql> CREATE DATABASE SLAVE;
mysql> USE SLAVE;
mysql> CREATE TABLE COUNTRY(ID INT NOT NULL PRIMARY KEY, CONTINENT CHAR(20), COUNTRY CHAR(20) NOT NULL);

mysql> INSERT INTO COUNTRY VALUES (1,'Asia','Korea');
mysql> INSERT INTO COUNTRY VALUES (2,'Asia','Japan');
mysql> FLUSH PRIVILEGES;

 

Slave DB

슬레이브 DB도 마찬가지로 적용 여부를 확인합니다.

 

2. application.properties 설정

생성한 데이터베이스에 대한 정보를 Spring Boot의 application.properties 파일에 아래와 같이 작성합니다.

# MASTER DB
spring.master.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.master.datasource.jdbc-url=jdbc:log4jdbc:mysql://192.168.0.10:3306/MASTER?characterEncoding=UTF-8&serverTimezone=UTC
spring.master.datasource.username={user name}
spring.master.datasource.password={password}
spring.master.datasource.connection-test-query=SELECT 1

# SLAVE 1 DB
spring.slave-1.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.slave-1.datasource.jdbc-url=jdbc:log4jdbc:mysql://192.168.0.13:3306/SLAVE?characterEncoding=UTF-8&serverTimezone=UTC
spring.slave-1.datasource.username={user name}
spring.slave-1.datasource.password={password}
spring.slave-1.datasource.connection-test-query=SELECT 1

{user name}, {password} 부분은 각자 개인의 데이터베이스 계정 정보를 의미합니다.

 

슬레이브 DB 설정에서 slave-1이라는 이름으로 설정값을 주었는데, 이는 슬레이브 DB는 얼마든지 여러 개가 올 수 있고, 나중에라도 추가가 될 수 있는 DB이기 때문에 그때를 고려한 넘버링입니다. (ex slave-1, slave-2, slave-3)

 

이때, STS 환경이라면 DB 설정 정보에 노란색 밑줄로 정의되지 않은 이름이라고 경고를 띄워줄텐데, 올바르게 구성한 정보이므로 무시합니다.

 

3. Sql 세션 구성 정보 작성

이제, 각 데이터베이스의 설정 정보를 토대로 SqlSession을 생성하고 트랜젝션이 이루어지도록 구성 설정을 해 주어야 합니다.

간단하게 생각하면 application.properties에서 정의한 DB 연결 정보를 적용하고 그 DB와 상호작용하는 mapper 파일을 정의하는 내용을 작성하는 것입니다.

 

위 사진과 같이 com.example.demo 경로에 config라는 패키지를 생성하고 안에 각각 MasterDataBaseConfig.java, Slave1DataBaseConfig.java 파일을 생성합니다.

 

그리고 각 파일에 아래와 같이 작성합니다.

 

MasterDataBase.Config.java

package com.example.demo.config;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
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 org.springframework.context.annotation.Primary;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@MapperScan(value="com.example.demo.mapper.master", sqlSessionFactoryRef="masterSqlSessionFactory")
@EnableTransactionManagement
public class MasterDataBaseConfig {
	
	@Primary
	@Bean(name="masterDataSource")
	@ConfigurationProperties(prefix="spring.master.datasource")
	public DataSource masterDataSource() {
		//application.properties에서 정의한 DB 연결 정보를 빌드
		return DataSourceBuilder.create().build();
	}
	
	@Primary
	@Bean(name="masterSqlSessionFactory")
	public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource masterDataSource, ApplicationContext applicationContext) throws Exception{
		//세션 생성 시, 빌드된 DataSource를 세팅하고 SQL문을 관리할 mapper.xml의 경로를 알려준다.
		SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
		sqlSessionFactoryBean.setDataSource(masterDataSource);
		sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:com/example/demo/mybatis/master/*.xml"));
		return sqlSessionFactoryBean.getObject();
	}
	
	@Primary
	@Bean(name="masterSqlSessionTemplate")
	public SqlSessionTemplate masterSqlSessionTemplate(SqlSessionFactory masterSqlSessionFactory) throws Exception{
		return new SqlSessionTemplate(masterSqlSessionFactory);
	}
	
}

 

Slave1DataBaseConfig.java

package com.example.demo.config;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
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 org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@MapperScan(value="com.example.demo.mapper.slave1", sqlSessionFactoryRef="slave1SqlSessionFactory")
@EnableTransactionManagement
public class Slave1DataBaseConfig {
	
	@Bean(name="slave1DataSource")
	@ConfigurationProperties(prefix="spring.slave-1.datasource")
	public DataSource masterDataSource() {
		//application.properties에서 정의한 DB 연결 정보를 빌드
		return DataSourceBuilder.create().build();
	}
	
	@Bean(name="slave1SqlSessionFactory")
	public SqlSessionFactory slave1SqlSessionFactory(@Qualifier("slave1DataSource") DataSource slave1DataSource, ApplicationContext applicationContext) throws Exception{
		//세션 생성 시, 빌드된 DataSource를 세팅하고 SQL문을 관리할 mapper.xml의 경로를 알려준다.
		SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
		sqlSessionFactoryBean.setDataSource(slave1DataSource);
		sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:com/example/demo/mybatis/slave1/*.xml"));
		return sqlSessionFactoryBean.getObject();
	}
	
	@Bean(name="slave1SqlSessionTemplate")
	public SqlSessionTemplate slave1SqlSessionTemplate(SqlSessionFactory slave1SqlSessionFactory) throws Exception{
		return new SqlSessionTemplate(slave1SqlSessionFactory);
	}
	
}

 

 

import 된 객체들을 확인하고 적용해 줍니다.

그리고 각 어노테이션에서 정의한 Ref와 name 정보를 꼼꼼하게 확인해 주어야 합니다.

대부분의 에러의 원인은 이 Config 파일에서 발생합니다.

 

MasterDataBaseConfig와 내용이 겹치면 안 되며, 각 어노테이션에서 참조하는 name과 각 메서드의 이름이 일치하는지 잘 확인합니다.

 

사실 이 작업이 완료되었다면 본 포스팅에서 목적한 바의 절반은 완료된 것입니다.

이제는 이 구성 스크립트에서 정의한 대로 올바른 경로에 올바른 이름으로 나머지 필요 파일들을 생성해 주어야 합니다.

 

4. 각 테이블에 대응할 Model 객체 정의

데이터베이스의 테이블 정보를 받을 수 있는 Model 객체를 정의하겠습니다.

com.example.demo 패키지에 model 패키지를 생성합니다.

그리고 마스터 DB의 Salary 테이블 정보를 받을 SalaryModel.java와 슬레이브 DB의 Country 테이블을 받을 CountryModel.java 파일을 생성하고 아래와 같이 정의합니다.

본 예제에서는 lombok을 사용했는데, 만약 lombok을 사용하지 않는 환경이라면 getter/setter 메서드를 각각 정의해줍니다.

 

SalaryModel.java - master

//SalaryModel.java

package com.example.demo.model;

import lombok.Builder;
import lombok.Data;
import lombok.NonNull;

@Builder
@Data
public class SalaryModel {
	private int id;
	@NonNull @Builder.Default private String name = "NULL NAME";
	@NonNull @Builder.Default private String email = "NULL EMAIL";
}

 

CountryModel.java - slave 1

// CountryModel.java

package com.example.demo.model;

import lombok.Builder;
import lombok.Data;
import lombok.NonNull;

@Builder @Data
public class CountryModel {
	private int id;
	@NonNull @Builder.Default private String continent = "No CONTINENT";
	@NonNull @Builder.Default private String country = "No Country";
}

 

 

5. Mapper 인터페이스 정의

이제, 쿼리문 요청하기 직전 단계인 Mapper 인터페이스를 정의합니다.

 

Mapper 인터페이스는 Config.java에서 MapperScan 어노테이션을 통해 각자 별도의 패키지에서 탐색하도록 정의했으므로 패키지를 각각 생성해야 합니다.

 

 

com.example.demo에 공통으로 사용할 mapper 패키지를 생성하고 그 안에 master, slave1 패키지를 추가로 생성합니다.

그리고 master 패키지 안에는 MasterDataBaseMapper.java 파일을, slave1 패키지 안에는 Slave1DataBaseMapper.java 파일을 생성합니다.

 

그리고 각 파일에 아래와 같이 내용을 작성합니다.

 

MasterDataBaseMapper.java

// MasterDataBaseMapper.java

package com.example.demo.mapper.master;

import java.util.List;

import com.example.demo.model.SalaryModel;

public interface MasterDataBaseMapper {
	public List<SalaryModel> getSalary() throws Exception;
}

 

Slave1DataBaseMapper.java

// Slave1DataBaseMapper.java

package com.example.demo.mapper.slave1;

import java.util.List;

import com.example.demo.model.CountryModel;

public interface Slave1DataBaseMapper {
	public List<CountryModel> getCountry() throws Exception;
}

 

위 파일들은 config.java에서 @MapperScan을 통해 mapper 파일의 위치를 알려주었으므로 @Repository @Mapper 어노테이션을 붙여줄 필요가 없습니다.

 

각 mapper 파일에는 DB에 있는 정보들을 조회하는 간단한 메서드만 정의해 주었습니다.

 

6. 각 테이블의 Mapper xml파일 정의

이제, 각 테이블에 요청할 쿼리문을 관리할 xml 파일을 생성해 주어야 합니다.

이 xml 파일도 데이터베이스 별로 패키지를 나누어 줍니다.

 

거듭 언급하지만 생성하는 패키지 경로는 confg.java에서 정의한 대로 생성해야 합니다.

mapper 인터페이스와 구분을 주기 위해 xml 파일들의 패키지는 mybatis를 상위 패키지로, master, slave1 패키지를 하위로 구성하고 master에는 MasterDataBaseMapper.xml 파일을, slave1에는 Slave1DataBaseMapper.xml을 생성합니다.

그리고 아래와 같이 각 파일에 작성합니다.

 

MasterDataBaseMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.demo.mapper.master.MasterDataBaseMapper">
	<select id="getSalary" resultType="com.example.demo.model.SalaryModel">
		SELECT * FROM SALARY;
	</select>
</mapper>

 

Slave1DataBaseMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.demo.mapper.slave1.Slave1DataBaseMapper">
	<select id="getCountry" resultType="com.example.demo.model.CountryModel">
		SELECT * FROM COUNTRY;
	</select>
</mapper>

 

7. 사용자 클래스에서 접근하기 위한 Service 정의

service 패키지를 생성하고 MasterDataBaseService.java와 Slave1DataBaseService.java 스크립트를 생성합니다.

위 두 파일은 클래스를 별도로 두어도 되고 같은 패키지에서 관리해도 무방합니다.

예제에서는 service 패키지에서 같이 관리하도록 하겠습니다.

아래의 내용을 작성합니다.

 

MasterDataBaseService.java

// MasterDataBaseService.java

package com.example.demo.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.mapper.master.MasterDataBaseMapper;
import com.example.demo.model.SalaryModel;

@Service
public class MasterDataBaseService {
	@Autowired
	MasterDataBaseMapper masterDataBaseMapper;
	
	public List<SalaryModel> getSalary() throws Exception{
		return masterDataBaseMapper.getSalary();
	}
	
}

 

Slave1DataBaseService.java

// Slave1DataBaseService.java

package com.example.demo.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.mapper.slave1.Slave1DataBaseMapper;
import com.example.demo.model.CountryModel;

@Service
public class Slave1DataBaseService {
	@Autowired
	Slave1DataBaseMapper slave1DataBaseMapper;
	
	public List<CountryModel> getCountry() throws Exception {
		return slave1DataBaseMapper.getCountry();
	}
	
}

 

8. Controller에서 Service 호출

@Controller
public class HomeController {
	@Autowired
	private MasterDataBaseService masterDataBaseService;
	
	@Autowired
	private Slave1DataBaseService slave1DataBaseService;
	
	@RequestMapping(value = "/home", method = RequestMethod.GET)
	public ModelAndView goHome(HttpServletRequest request) throws Exception {
		
		ModelAndView mav = new ModelAndView();
		
		List<SalaryModel> salaryList = masterDataBaseService.getSalary();
		List<CountryModel> countryList = slave1DataBaseService.getCountry();
		
		mav.addObject("salaryList", salaryList);
		mav.addObject("countryList",countryList);
		mav.setViewName("content/home.html");
		return mav;
	}
	
}

 

본 예제에서는 /home 주소로 접속했을 때, 각기 다른 DB 테이블의 정보를 요청 해 페이지에 출력하도록 구성했습니다.

Service 스크립트는 Bean 객체이기 때문에 반드시 @Autowired 어노테이션으로 주입해야 합니다.

 

9. View 페이지에 파싱

컨트롤러에서 전달한 데이터를 확인하기 위해 아래와 같이 적절히 태그를 작성합니다.

아래의 스크립트는 thymeleaf 템플릿에 대한 예제이며, JSP를 이용하시는 분들은 JSTL 태그로 적절히 작성해 주세요.

<!-- home.html -->
<h3>Master DB in Salary Table</h3>
<table border="1">
    <tr>
        <th>id</th>
        <th>name</th>
        <th>email</th>
    </tr>
    <th:block th:each="salary : ${salaryList}">
    <tr>
        <td th:text="${ salary.id }"></td>
        <td th:text="${ salary.name }"></td>
        <td th:text="${ salary.email }"></td>
    </tr>
    </th:block>
</table>

<hr>

<h3>Slave1 DB in Country Table</h3>
<table border="1">
    <tr>
        <th>id</th>
        <th>continent</th>
        <th>country</th>
    </tr>
    <th:block th:each="country : ${countryList}">
    <tr>
        <td th:text="${ country.id }"></td>
        <td th:text="${ country.continent }"></td>
        <td th:text="${ country.country }"></td>
    </tr>
    </th:block>
</table>

 

이제 컨트롤러에서 정의한 주소로 접속해 봅니다.

 

만약 log4jdbc를 적용한 프로젝트라면 아래와 같이 데이터 로그를 출력해 줄 것입니다.

 

 

로그에서 각기 다른 두 개의 테이블 정보를 보여주고 있습니다.

 

 

페이지에서도 정상적으로 데이터를 출력하고 있음을 확인했습니다.

 

개인적으로는 아직까지는 이렇게 하나의 애플리케이션에서 여러 개의 데이터 베이스를 동시에 이용하는 프로젝트를 경험해보지 못했습니다.

하지만, 프로젝트의 성격에 따라 특히나 Spring과 같은 엔터프라이즈 프레임워크에서는 이와 같은 데이터 베이스 다중 연동 기능이 필요한 순간은 반드시 올 수 있으므로 지금 당장은 활용하지 못하더라도 한번쯤은 구현해 보는 것도 나쁘지 않은 시스템인 것 같습니다.

 

이것으로 Mybatis에서의 DataBase 다중 연결 포스팅을 마치겠습니다.