[Spring boot] 다중 DB(Multiple DB) 연결 세팅


프로젝트를 시작하며 업무에서 2개의 DB를 하나의 서버 프로젝트에 커넥션을 잡아줘야 하는 일을 맡게됬습니다.

항상 토이프로젝트를 하든 팀 프로젝트를 하든 하나의 DB를 가지고 세팅을 했기 때문에 구글링도 여러번 해보고 헷갈렸는데요. 혹시나 까먹고 두번 다시 이걸로 고생하고싶지 않아서 정리해두려고합니다. 

 

우선 순서를 정리해보면

 

1. 메인 DB와 서브 DB 정하기

 

2. build-gradle에 connector 추가

-- log4jdbc는 추가 설정파일(.properties) 추가

 

3. 각 DB의 config 클래스 생성 후 DB 정보 입력

 

3-1. 메인 DB와 서브 DB에 따른 설정 추가

 

4. mapper에 각 DB SQL문 작성

 

5. DAO에 메인,서브DB Bean정상 호출 확인

 

6. 웹 실행을 통한 최후 테스트

 

 

이런식으로 갑니다.

 

우선 메인 DB와 서브 DB를 정하는 이유는 잠시 뒤 보실 설정부분과 호출부분에서 Primary로 설정하는 DB는 설정이 간단하고 호출도 1개의 DB만 사용할 때 그대로 사용하면 되기때문에 자주 사용하시는 DB를 메인 DB로 정하시고 가시는게 설정시 매우 편합니다.

 

이제 시작해보겠습니다.

 

 

1. 메인/서브 DB 정하기

우선 저는 프로젝트에서 MySQL과 Tibero를 사용하기로 했는데, 자주 사용되는 DB가 Tibero이기 때문에 메인 DB는 Tibero, 서브 DB는 MySQL로 사용하겠습니다.


2. 라이브러리에 connector 추가 및 log4jdbc 설정 파일 추가


  - 우선 build.gradle의 dependencies에 필요한 DB커넥터들을 추가 후 빌드합니다.

log4jdbc쓰시는 분들은 같이 ㄱㄱ~
외부 라이브러리에서 확인하기!

  - log4jdbc.log4j2.properties 파일 생성

- src/main/resources 아래에 log4jdbc.log4j2.properties라는 파일을 생성 후 아래와 같이 입력합니다.

   

- log4jdbc는 기존 DB 커넥터를 감싸 로그 기능을 추가해주는 개념이기때문에.. 베이스로 어떤 DB 커넥터를 사용할지          등을 설정해주는 파일이 필요합니다. 저는 두 개의  DB에 모두 로그를 남겨야 하기때문에, 커넥터를 두개 다 넣었습니        다.  만약 둘중 하나만 설정하고 싶으시다면, 설정하고 싶은 DB의 원래 커넥터를 입력하시면 되겠습니다.


3. Config 클래스 생성 후 설정


  - xml등을 사용하여 설정하는 방법도 있지만 스프링은 어노테이션을 사용하면 쉽게 Bean설정을 할 수 있기 때문에

    Config 클래스를 통해 설정해보겠습니다.

( 사실은 이렇게 해보고도 싶고 xml 가독성이 너무 떨어진다고 개인적으로 생각되어 사용했습니다.)

 

파일은 src/main/java/com.xxx.xx의 MVC구조 파일들이 모여있는 곳에 넣어야합니다. 기본적으로 Spring이 실행될 때 src/main/java안에 있는 파일들을 컴파일해 스프링 컨테이너에 Bean을 등록하기때문에, 별도 설정없이 해당 디렉터리를 벗어나면 Bean 자체를 인식하지 못하는 경우가 생깁니다.

 

우선 아까 정한 메인 DB 부터 보겠습니다

package com.example.samyuk.config;

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.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.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

@Configuration
public class TiberoDataSourceConfig {
    @Bean
    @Primary // setting Multyple DataBase Connection by sangU
    public DataSource tiberoDataSource() {

        return DataSourceBuilder.create()
                .url("jdbc:log4jdbc:tibero:thin:@IP:PORT:DBNAME") // URL을 명시적으로 지정
                .driverClassName("net.sf.log4jdbc.sql.jdbcapi.DriverSpy") // 드라이버 클래스명을 명시적으로 지정
                .username("DB유저 id")
                .password("DB비밀번호")
                .build();
    }

    @Bean
    @Primary
    public SqlSessionFactory firstSqlSessionFactory(DataSource firstDataSource, ApplicationContext applicationContext) throws Exception {

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(firstDataSource);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:mappers/*.xml"));//xml파일의 위치, src/main/resources아래에 위치
        return sqlSessionFactoryBean.getObject();
    }

    /**
     * DAO 클래스에서 사용한다.
     **/
    @Bean
    @Primary
    public SqlSessionTemplate firstSqlSessionTemplate(SqlSessionFactory firstSqlSessionFactory) throws Exception {
        final SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(firstSqlSessionFactory);
        return sqlSessionTemplate;
    }
}

코드 자체는 DB 1개를 연동 할때와 비슷하지만 자세히 보면 @Primary라는 어노테이션이 있습니다. 저도 이번 일을 겪으면서  처음 마주한 어노테이션인데요. 스프링 컨테이너에 Bean들을 등록할 때, 동일한 Bean이 두 개 등록 되있으면 어떤 Bean을 가져야할지 모릅니다. 때문에 에러가 발생되는데요. Primary 어노테이션을 붙여놓으면 다른곳에서 Bean 선언시 아무설정이 없다면 기본적으로 Primary어노테이션이 붙은 Bean으로 자동 주입됩니다.

 

때문에 메인 DB와 서브 DB를 정해 놓으라고 저는 말씀드리고 싶었는데요, 만약 자주 사용하는 DB를 Primary로 놓지 않으면 선언시마다 설정을 해주어야합니다. 이러면 효율 자체도 좋지 않고 한 두줄 추가되는 것도 좋지 않을 수 있다고 생각합니다.

 

이제 서브 DB의 config클래스를 살펴보겠습니다.

package com.example.samyuk.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
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.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

@Configuration
public class MariaDataSourceConfig {
    @Bean(name = "mariaDataSource")
    public DataSource mariaDataSource(){
        return DataSourceBuilder.create()
                .url("jdbc:log4jdbc:mysql://127.0.0.1:3306/test_db")
                .driverClassName("net.sf.log4jdbc.sql.jdbcapi.DriverSpy")
                .username("root")
                .password("12345")
                .build();
    }

    @Bean(name = "mariaSqlSessionFactory")
    public SqlSessionFactory mariaSqlSessionFactory(@Qualifier("mariaDataSource") DataSource dataSource) throws Exception {

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mappers/*.xml"));//xml파일의 위치, src/main/resources아래에 위치
        return sqlSessionFactoryBean.getObject();
    }



    /**
     * DAO 클래스에서 사용한다.
     **/
    @Bean(name = "mariaSqlSessionTemplate")
    public SqlSessionTemplate mariaSqlSessionTemplate(@Qualifier("mariaSqlSessionFactory") SqlSessionFactory mariaSqlSessionFactory) throws Exception {
        final SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(mariaSqlSessionFactory);
        return sqlSessionTemplate;
    }
}

크게 다를건 없지만  SessionFactory와 SqlSessionTemplate객체의 인자에 @Qualifier 어노테이션이 존재하는데요, @Qualifier("Bean명")을 하면 괄호 안의 문자에 맞는 이름을 가진 Bean을 가져옵니다. 해당 밑 두개도 서브 DB config클래스에 들어가기때문에, 어떤 DB의 커넥션 DataSource를 넣을지 정해주어야 SqlSessionTemplate선언시 원하는 DB에 붙습니다.

(저는 여기서 한참 해멨습니다...)

 

* 또한 Bean 어노테이션에 name 속성을 추가하면 후에 선언시 해당 속성명으로 Bean을 부를 수 있습니다! 지정하지 않으면 메소드명으로 세팅됩니다.( 따라서 본문의 코드에는 넣으나 안넣으나 마찬가지입니다.)


4. Mapper에 각 DB SQL문 작성


우선 Mapper까지 구분할 필요는 없으나, SQL문법이 조금 다른 언어의 경우 헷갈릴 수 있으니 실제로 사용하실땐 따로 사용하시는 것도 괜찮은거 같습니다. 하지만 해당 글에서는 테스트전용으로 만들기 때문에 같은 mapper에 작성하겠습니다!

 

파일 구조

<?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="mappers.ExampleMapper">
    
    <select id="try36" resultType="com.example.samyuk.vo.CategorySetListVO">
        select 
            *
        from
            CONSENT_CATEGORY_SET_LIST
    </select> <-- 이거 tibero꺼
   
   <select id="testMultiple" resultType="com.example.samyuk.vo.MultipleConnTestVO">
        select
            *
        from
            test_db.user u
    </select> <-- 이거 MySQL

</mapper>

이건 Mybatis를 사용하시는 분들에게 매우 익숙한 부분이라 추가 설명은 하지 않겠습니다.


5. DAO호출 후 정상 작동 확인


DAO에 호출 후 정상적으로 import되는지 확인할 단계입니다. 우선 선언부 코드입니다.

 

package com.example.samyuk.DAO;

import com.example.samyuk.vo.*;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class TestDAO {

    @Autowired
    private SqlSession sqlSession; // mainDB @Primary로 인해 직접 지정해줄 필요없음

    @Autowired
    @Qualifier(value = "mariaSqlSessionTemplate") // 같은 bean이 2개 이상 존재할 시 어떤 bean을 사용할 건지 지정해야함.
    private SqlSession sqlSession2;         
    // subDB
    public List<MultipleConnTestVO> getTestConn(){
        return sqlSession2.selectList("mappers.ExampleMapper.testMultiple");
    }
    
    public List<CategorySetListVO> get36Test(){
        return sqlSession.selectList("mappers.ExampleMapper.try36");
    }
    
    }

 

보시면 메인DB(Tibero)를 바라보는 SqlSession객체는 자동주입 어노테이션만 존재합니다. 이럴경우 @Primary 어노테이션이 붙은 Bean이 주입됩니다.

 

서브DB(MySQL)을 바라보는 sqlSession2 객체는 @Qualifier 어노테이션을 통해 mariaSqlSessionTemplate이라는 이름의 Bean을 주입합니다. 그렇게 두 개를 작성하고 각 DB에 맞는 SQL Mapper문을 통해 실행하면 됩니다.

 

다들 아시겠지만 IntelliJ나 Eclipse같은 경우 @Qualifier의 value값을 ctrl + 좌클릭시 제대로 연동되면 저희가 설정한 Config클래스로 이동됩니다.

 

그리고 나머지 구조를 통해 DB의 데이터가 잘 출력되는지 확인해보면 됩니다. view를 따로 만들기는 좀 번거로워서 ResponseBody로 전송값만 확인했습니다.

 

다 정상 출력됬네여

 

이상 다중 DB (Multiple DataBase) 연동방법에 대해 알아봤습니다. 혹시 자세한 개념에서 틀린부분이나 지적사항 있으시면 댓글로 말씀해주시면 감사하겠습니다.

 

 

 

 

JPA 연동하기

JDK : 1.8
Spring Boot : 2.3.1

JPA : Hibernate

DBMS : MySQL

 

오늘은 JPA연동하는 방법에 대해서 포스팅을 하려고합니다.

단순 연동 방법이 필요하신 분들은 서론을 건너뛰고 보시면 될 것같습니다.

JPA란?

JPA란 MyBatis랑 비슷하게 DB와 서버의 연결을 간편하게 해주는 인터페이스입니다. 하지만 Mybatis와 다르게 엔티티에 직접 테이블과 컬럼을 지정하고 쿼리문을 객체지향적으로 코딩할 수 있게 도와줍니다.

 

JPA의 구성

JPA의 역할을 이해하기 쉽도록 그림을 한장 퍼왔습니다. 위 설명과 조합하시면 이해가 빠르실 것 같습니다.

출처 : https://velog.io/@tmdgh0221/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-%EC%A0%95%EB%A6%AC

그림을 보시면 JPA라는 인터페이스로 JDBC를 감싸고 있습니다. 저희가 JPA의 함수와 문법을 이용해서 쿼리문을 날려달라고 요청하면, JPA는 JDBC에 맞게 해당 요청을 쿼리문으로 변환하여 DB와 연동합니다. 

 

왜 MyBatis에서 JPA로 넘어가나요?

개인적인 생각으로는 MyBatis는 Mapping파일을 경로로 잡는 config파일을 xml로 만들고 또한 Mapper파일을 새로 생성하여 쿼리문을 작성하기때문에 객체지향적인 설계와는 거리가 멀고, 엔티티를 쉽게 사용하려면 별칭으로 등록하고 또한 그마저도 사소한 오타가 존재할 시 오류가 발생합니다. JDBC를 직접 사용하는 것보다는 낫겠지만 그래도 개발시에 상당히 힘들고, 유지보수 역시 하나하나 뒤져보면서 Entity의 값과 name을 비교하는것이 많이 힘듭니다.

 

하지만 JPA는 엔티티에서 그대로 컬럼, 테이블을 지정하고, MVC의 Model부분에서 객체지향적으로 쿼리문을 날리면 작성자의 목적에 맞게 변환시켜 데이터를 CRUD할 수 있게 되어있기 때문에 간편하고, 세팅이나 오류나 발생할 부분이 적어서 확실히 개발 효율을 증가시킬 수 있을 것같습니다.

 

Hibernate(JPA 구현체)

오늘 포스팅에서는 Hibernate라는 JPA 인터페이스의 구현체를 사용할 것입니다. 위 설명대로 JPA는 인터페이스 이기때문에, 작성되어있는 메서드를 구현해야합니다. 이런 것들을 이미 구현해놓은 구현체를 사용함으로서 쉽게 사용할 수 있습니다. 물론 Hibernate말고도 EclipseLink, DataNucleus, OpenJPA, TopLink Essentials등 여러가지가 존재하니, 상황에 맞게 사용하시면 되겠습니다.

 

JPA 연동방법

우선 스프링 부트 프로젝트를 생성해야합니다. 모르시는 분들은 밑의 글을 참고해주세요, 여러 설정중에 JPA 연동과 관련된 라이브러리는 Spring-data-JPA와 사용할 DB의 connector입니다. 참고 Spring-data-JPA만 넣으시면 자동으로 hibernate에 대한 라이브러리도 추가됩니다.

2023.07.14 - [JAVA] - [Spring] Spring boot 프로젝트 생성 방법

 

[Spring] Spring boot 프로젝트 생성 방법

Spring boot란? - Spring Boot란 Spring Framework 프로젝트 생성시 처음하면 매우매우 복잡하고 절대로 쉽게 할 수 없는 초기 설정 및 라이브러리 세팅을 간단하게 만들어주는 프레임워크(FrameWork)입니다. -

lucky-web.tistory.com

Spring Data JPA와 Hibernate에 대한 라이브러리 추가
DB 커넥터 라이브러

 

그 이후 생성된 프로젝트의 외부 라이브러리를 확인하면, hibernate와 JPA에 대한 라이브러리를 확인할 수 있습니다.

hibernate관련 라이브러리
Spring Data JPA 관련 라이브러리

 

외부 라이브러리를 확인하셨다면 src/resources 디렉터리 밑에 application.properties파일을 생성하고 해당 내용을 작성합니다.

application.properties추가

 

application.properties 내용

하나씩 보시면 server.port는 연결할 서버의 포트, url은 jdbc와 커넥션할 DB 스키마의 주소(JDBC에 사용하는 url이나 Mybatis에서 설정해놓은 url과 같습니다.) 유저 아이디와 비밀번호, 그리고 연결할 때 사용하는 드라이버 주소까지 입력합니다.

 

그리고 그 밑은 어떤 DB를 사용하는지 지정하는 부분이고, Database-platform역시 같습니다.

 

3번째는 어떤 단계의 로그부터 남길지를 정하는 단계로, log4j의 여러단계에서 용도에 맞게 지정하시면됩니다.

 

이런 설정들은 더 많이 있지만 필요할 때 마다 알아보시면서 추가하시면 좋을 것같습니다.

 

 

 

 

이제는 엔티티(Entity)와 DB 테이블을 연동하겠습니다. 우선 가상의 테이블을 만들겠습니다.

가상의 테이블

member테이블은 pid,username,name 총 3가지의 컬럼을 가지고 있는 테이블입니다. 해당 테이블을 연결하는 엔티티는 당연히 컬럼의 갯수에 맞게 만들어야겠죠?

@Getter // lombok
@Builder // lombok
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name="member")// 테이블 명 작성
public class MemberEntity {

    @Id // pk
    @GeneratedValue(strategy = GenerationType.IDENTITY) // == 시퀀스
    private long pid;

    @Column(nullable = false,unique = true,length = 30) // 속성값 정하기
    private String username;

    @Column(nullable = false, length = 100)
    private String name;

    public MemberEntity(String username, String name) {
        this.username = username;
        this.name = name;
    }
}

테이블 member를 바라보는 MemberEntity클래스를 생성했습니다. @Entity(name = "member")어노테이션은 연결된 DB의 member라는 이름을 가진 테이블과 연동한다는 뜻입니다.

- @Id는 해당 컬럼이 pk라는 것을 의미합니다.

- @GeneratedValue(strategy = GenerationType.IDENTITY)는 MySQL의 auto_increment나 Oracle의 sequence와 같이 자동 숫자 증가를 나타내게 하는 어노테이션인데, GenerationType을 바꿈에 따라 여러가지로 조정할 수 있습니다.

- @Column은 테이블의 컬럼이라는 뜻을 나타내고, 컬럼의 여러가지 속성을 제어할 수 있습니다.

 

이제 JPA의 기능을 사용하기 위한 준비를 하겠습니다.

 

JPARepository생성

 

 

Interface생성

JPA를 끌어올 인터페이스를 생성하고, JPARepository를 상속받습니다.

package com.example.demo.repo;

import com.example.demo.entity.MemberEntity;
import org.springframework.data.jpa.repository.JpaRepository;


public interface MemberRepository extends JpaRepository<MemberEntity, Long> {

}

따로 입력하시지 않아도 됩니다. 그저 JPA의 기능을 끌어쓰기 위한 인터페이스이기때문입니다.

 

 

JPA 작동

 

이제 JPA를 사용하기만 하면됩니다. 보통은 Service에 구현하여 데이터를 가져오고 원하는 방식으로 수정하기도 하지만, 지금은 작동을 보여드리기위하여 Controller에 바로 JPARepository의 의존성을 주입하고 사용해보겠습니다.

 

Controller

package com.example.demo.controller;

import com.example.demo.entity.MemberEntity;
import com.example.demo.repo.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController// @ResponseBody + @Controller
@RequiredArgsConstructor // final 객체를 Constructor Injection해줌.( like @Autowired)
@RequestMapping("/v1")
public class MemberController {
    private final MemberRepository repository;
    /*
    * 멤버조회
    * @return
    * */

    @GetMapping("member")
    public List<MemberEntity> findAllMember(){
        return repository.findAll();
    }

    /**
     * 회원가입
     *
     * @return
     */
    @PostMapping("member")
    public MemberEntity signUp() {
        final MemberEntity member = MemberEntity.builder().username("test_user@gmail.com")
                                                          .name("test user")
                                                          .build();
        return repository.save(member);
    }
}

코드를 보시면 방금 생성한 MemberRepository의 의존성을 주입하고, JPA에서 findAll이라는 함수를 사용하여 해당 값을 responseBody에 return하도록 되어있습니다.

findAll함수를 사용하면 어떤 쿼리문이 작동될까요?

우선 인터페이스 코드를 다시보면...

package com.example.demo.repo;

import com.example.demo.entity.MemberEntity;
import org.springframework.data.jpa.repository.JpaRepository;


public interface MemberRepository extends JpaRepository<MemberEntity, Long> {

}

MemberEntity를 JpaRepository에 담음으로서 해당 인터페이스는 member테이블을 바라보게 되어있습니다.

findAll은 해당 테이블의 모든 데이터를 조회하기때문에

select

          *

from

          member;

쿼리문이 실행되는 것입니다.

그럼 현재 DB에있는 데이터와 웹에서 리턴받는 데이터가 같은지 확인해보겠습니다.

5개의 데이터가 존재한다.

총 5개의 데이터가 존재하는것을 볼 수 있습니다. 그럼 웹에서 findAll()함수를 사용하는 페이지로 가서 5개가 나오는지 확인해보겠습니다.

 

총 5개가 나오는 것을 볼 수 있습니다. 이렇게 JPA의 세팅과 사용방법에 대해 다뤄보았습니다.

+ Recent posts