본문 바로가기
Spring/DB Migrator

[DB Migrator] Spring Boot에 2개 이상의 DataSource 붙이기

by Taler 2022. 9. 6.

2022.08.30 - [Spring] - [DB Migrator] Spring Boot와 함께하는 DB Migrator 프로젝트 - 1

 

[DB Migrator] Spring Boot와 함께하는 DB Migrator 프로젝트 - 1

이전에 인턴십을 진행했던 회사에서는 Spring에서 NestJS로의 리팩토링을 담당했었다. 해당 팀에 스프링 개발자가 없어 유지보수에 어려움을 겪던 것이 그 이유. 인턴십 기간 동안 생소한 Typescript

taler.tistory.com

이전 글을 보면 알 수 있듯, MongoDB의 데이터를 Postgres로 옮기는 작업을 수행하고자 했고, 이를 Spring-data-mongo, Spring-data-jpa의 도움을 받아 진행하고자 했다. 즉, Spring에 2개 이상의 DB를 연결해야 했다.

 

Spring에 여러 개의 데이터베이스를 연결하는 소스는 생각보다 많지 않다. 그리고, JPA를 쓰면서 MongoDB도 연결하고자 한다면 더욱더 줄어든다. 이번 포스팅에서는 Spring에 Mongo와 JPA 모두를 연결하는 방법에 대해서 공유해보고자 한다.

 

개발 환경


사용한 모든 것의 버전 정보는 다음과 같다. (아주 따끈따끈하다.)

  • Spring Boot 2.7.3
  • Java 17
  • 기타 패키지는 모두 최신

버전 정보를 명시하지 않는 것은 좋은 습관이 아닌것 같긴 하다..

 

비록 좋은 습관이 아닌 것은 맞지만, 시간 관계상 버전 세팅은 최후의 최후로 미룰 것 같다.

이제 본격적으로 다중 DB, DataSource를 쓰기 위해서 어떤 설정의 과정을 거쳤는지 알아보자.

 

 

스프링 설정 방법 및 Configuration의 선택 이유


스프링에서 DataSource는 기본적으로 하나의 Bean이다. 때문에 BeanDefinition에 등록이 되어 있어야 IoC 컨테이너에서 어떤 오브젝트를 생성할지 결정할 수 있다.

 

즉, BeanDefinition을 만들어서 IoC 컨테이너에게 건네주면, 빈을 등록할 수 있다. 원시적으로는 직접 BeanDefinition 구현 오브젝트를 직접 생성하여 만드는 것이 있지만, 이는 학습 시간 대비 너무 비효율적이다.

 

다른 더 편한 방법으로는 application.yml, .properties 파일, XML 파일, 클래스 위에 붙어있는 각종 Annotation들, 그리고 Configuration에 관한 자바 코드 등 다양한 방법으로 BeanDefinition 메타 정보를 작성하고 이를 적절한 Reader나 변환기를 통해 Application Context가 사용할 수 있도록 변환해주는 과정을 거친다.

 

크게 5가지가 있는데, 굳이 다 알 필요도 없고 (알면 좋다) 이 포스팅 역시 스프링 빈 등록에 관한 글이 아니다. 따라서 프로젝트에서 사용한 방법인 Configuration 코드에 대해서만 간략히 소개하고 넘어간다.

 

Configuration 코드는 @Configuration 어노테이션을 붙여서 오브젝트 팩토리처럼 스프링 빈을 Java 코드를 통해 직접 Bean을 생성해서 등록해주는 파일이라고 생각하면 된다. Configuration 코드는 오브젝트를 생성하고 DI를 진행해 오브젝트를 빈으로 직접 등록해 사용할 수 있도록 만들어졌다. 아래와 같은 장점들이 있다.

 

1. 직관적이다.
  - yml 파일 등은 어떤 이유로 해당 부분이 어떻게 등록되는지 알 수 없다.
  - 때문에 다른 사람들이 만든 코드를 그대로 따라치면서 '아~ 그냥 되는구나' 하고 넘어가게 된다.
  - 소스가 많은 것은 아니지만, 또 없는 것은 아니며 해봤자 setUrl() 같은 함수들이라 어렵지도 않다. (코드로 만나보자)

2. 튜닝이 쉽다.
  -  Java기 때문에 우리에게 더 친숙하며, 튜닝도 용감하게 도전할 수 있게 된다.
  -  코드의 각 부분이 어떤 역할을 하는지 파악하는 것이 쉽기 때문에 변경도 용이해진다.

3. 확장성
  - 하나의 Configuration 클래스에서 여러 개의 Bean을 등록할 수 있다.
  - 또한, 다른 Configuration 코드를 보면서 구성하기 쉽기 때문에 확장 측면에서도 훨씬 용이하다.

 

사실 두 번째 이유가 가장 컸다. 지금 제작하는 것은 아직은 Mongo To PostgreSQL Data Migration이지만, Postgres나 Mongo가 아닌 다른 DB를 (예를 들어 Postgres To MariaDB, MariaDB To Mongo 등등..) 사용하고자 하는 사용자에게도 쉽게 Spring 설정을 수정하고, 사용할 수 있는 방법이 이 Configuration 코드 방법이기에 선택했다.

 

이제 본격적으로 application.yml과 Config 파일들을 봐보자.

 

Configuration 코드


application.yml

 

spring:
  legacy:
    host: localhost
    port: 27018
    base-package: fromdb
    username: root
    password: root
  migration:
    host: localhost
    port: 5433
    dbname: todb
    dbschema: public
    username: root
    password: root

 

application.yml이 갑자기 나와서 당황했을 수 있지만, 다른 것들과 다르게 어떤 Spring 내부 메서드와 상호작용하는 등의 일은 없다. 단지 Configuration 코드에서 Value 값으로 사용하기 위해서 작성했다.

 

사실 application.yml로 Multi Database 연결에 성공했지만, 정확히 어떤 과정을 거쳐서 이게 스프링 빈으로 등록되는지 까지 알 턱이 없었다. 때문에 Configuration 코드로 직접 빈을 생성해서 넣어준 것. application.yml로 작성해둔 세팅도 남겨두긴 했다.

 

아무튼, 본격적으로 DBConfig 파일들을 보기 전에 해당 프로젝트에 대한 배경지식을 다시 한 번 상기해보자.

 

LegacyDB: 레거시 DB. 이번 phase에서는 MongoDB이며, 이후 범용 설정을 추가해 수정하고자 한다.
MigrationDB: 레거시 DB의 데이터를 옮길 도착지 DB. 이번 phase에서는 PostgreSQL이며, 지금도 Jpa가 지원하는 그 어떤 DB로도 변경하는 것은 쉽다.

 

Legacy와 Migration은 코드 외에도 임시로 작성한 예시 코드에서도 많이 등장한다. 그럼 이제 MongoDB인 LegacyDB의 Configuration 코드를 봐보자.

 

 

LegacyDBConfig.java


가장 먼저 LegacyDB인 MongoDB와 연결하는 방법을 알아보자. 다음은 Spring 공식 문서의 spring-data/mongo의 'Spring을 사용하여 MongoDB에 연결' 파트를 가져온 것이다.

 

One of the first tasks when using MongoDB and Spring is to create a com.mongodb.client.MongoClient object using the IoC container. There are two main ways to do this, either by using Java-based bean metadata or by using XML-based bean metadata. Both are discussed in the following sections.

MongoDB와 Spring을 사용할 때 해야하는 첫 번째 작업 중 하나는 IoC Container를 사용해 com.mongodb.client.MongoClient 객체를 생성하는 것입니다. Java 기반의 bean metadata를 사용하거나 XML 기반의 bean metadata를 활용하는 방식으로 생성할 수 있습니다. 둘 다 다음 섹션에서 설명합니다.

 

즉, JPA에서의 DataSource 역할을 MongoDB에서는 mongoClient가 담당한다는 것이다. 이를 생성하는 방법은 Java Code 기반이 있고, XML 기반이 있지만 앞서 언급했다시피 필자는 자바 기반의 코드로 생성했다.

 

package com.dbmigrator.DBMigrator.config;

import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableMongoRepositories(
        basePackages = {"com.dbmigrator.DBMigrator.domain.legacy"} // MongoDB가 매핑할 Entity 가 있는 패키지 위치
)
@ComponentScan(basePackages = {"com.dbmigrator.DBMigrator.domain.legacy"})
public class LegacyDBConfig {
    @Value("${spring.mongodb.host}")
    private String legacyDBHost;

    @Value("${spring.mongodb.port}")
    private String legacyDBPort;

    @Value("${spring.mongodb.base-package}")
    private String legacyDBBasePackage;

    @Value("${spring.mongodb.username}")
    private String legacyDBUsername;

    @Value("${spring.mongodb.password}")
    private String legacyDBPassword;

    @Bean
    public MongoClient mongoClient() {
        MongoClientSettings.Builder clientSettingsBuilder = MongoClientSettings.builder()
                .applyToSocketSettings(builder -> {
                    // Timeout Configurations
                    builder.connectTimeout(1000, TimeUnit.MILLISECONDS);
                    builder.readTimeout(1000, TimeUnit.MILLISECONDS);
                })
                .applyConnectionString(new ConnectionString("mongodb://" + legacyDBUsername + ":" + legacyDBPassword + "@" + legacyDBHost + ":" + legacyDBPort + "/?authSource=admin"));

        return MongoClients.create(clientSettingsBuilder.build());
    }
}

 

@Value 어노테이션은 application.properties나 application.yml에 명시된 값을 가져오는 용도로 사용된다. 

 

MongoClient를 생성해주는 부분을 보자. 해당 부분에서는 주어진 메타정보를 바탕으로 Client 오브젝트를 만들고 빈에 등록한다. 해당 클라이언트 세팅 빌더와 같은 경우 "spring MongoClientSettings configuration" 등으로 검색하면 관련 소스가 나온다.

 

해당 접근 방식을 사용하면 Spring의 MongoClientFactoryBean을 사용해 표준 MongoClient 인스턴스를 생성해 사용할 수 있다는 점이다. 직접 MongoClient를 초기화하는 것과 비교했을 때, FactoryBean은 ExceptionTranslator가 추가된 컨테이너를 제공한다는 이점을 갖는다.

 

참고로 ExceptionTranslator는 MongoDB 예외를 @Repository annotation이 달린 DAO 클래스에 대한  Spring의 portable DataAccessException(혹은 이를 상속하는 예외)로 변환하는 기능을 담당한다. 자세한 내용은 이곳을 참조.

 

만약 여러 대의 MongoDB에 연결해야 한다면, 아래와 같이 여러 개의 Bean을 생성해줘야 한다.

 

{
    ...
    
    @Bean(name = "legacyMongoClient")
    public MongoClient legacyMongoClient() {
        MongoClientSettings.Builder clientSettingsBuilder = MongoClientSettings.builder()
                .applyToSocketSettings(builder -> {
                    // Timeout Configurations
                    builder.connectTimeout(0, TimeUnit.MILLISECONDS);
                    builder.readTimeout(0, TimeUnit.MILLISECONDS);
                })
                .applyConnectionString(new ConnectionString("mongodb://" + legacyDBUsername + ":" + legacyDBPassword + "@" + legacyDBHost + ":" + legacyDBPort + "/?authSource=admin"));

        return MongoClients.create(clientSettingsBuilder.build());
    }
    
    @Bean(name = "migrationMongoClient")
    public MongoClient migrationMongoClient() {
        MongoClientSettings.Builder clientSettingsBuilder = MongoClientSettings.builder()
                .applyToSocketSettings(builder -> {
                    // Timeout Configurations
                    builder.connectTimeout(0, TimeUnit.MILLISECONDS);
                    builder.readTimeout(0, TimeUnit.MILLISECONDS);
                })
                .applyConnectionString(new ConnectionString("mongodb://" + migrationDBUsername + ":" + migrationDBPassword + "@" + migrationDBHost + ":" + migrationDBPort + "/?authSource=admin"));

        return MongoClients.create(clientSettingsBuilder.build());
    }
    
    ...
}

 

바로 이런 식.

 

MongoClient는 MongoDB driver API를 추상화시킨 인터페이스로써 그 자체만으로도 DB url과 database name, username 등의 정보를 담아 특정 MongoDB 인스턴스에 연결할 수 있다. 하지만 필자는 해당 과정을 MongoDatabase 객체로 수행하는 것을 추천한다. MongoDatabase 객체로 연결을 생성하는 경우 해당 MongoDB 인스턴스의 모든 기능에 액세스할 수 있다.

 

Spring은 이 MongoDatabase를 위해 MongoDatabaseFactory 인터페이스를 제공한다. MongoDatabaseFactory 인터페이스는 MongoTemplate을 구성하는데에도 사용할 수 있다.

 

하지만 일단은 MongoClient로도 수행할 수 있으니, 최적화를 위해 MongoClient로 연결했다. 약간 험난했지만, 그래도 충분히 할만한 일이었다.

 

이제 MigraitonDBConfig를 봐보자.

 

 

MigrationDBConfig


package com.dbmigrator.DBMigrator.config;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

@Configuration // Spring Configuration 임을 명시하는 annotation
@EnableTransactionManagement
@EnableJpaRepositories(
        basePackages = { "com.dbmigrator.DBMigrator.domain.migration" }// Postgres 가 매핑할 Entity 가 있는 패키지 위치
)
@ComponentScan(basePackages = {"com.dbmigrator.DBMigrator.domain.migration"})
public class MigrationDBConfig {
    @Value("${spring.postgres.host}")
    private String migrationDBHost;

    @Value("${spring.postgres.port}")
    private String migrationDBPort;

    @Value("${spring.postgres.dbname}")
    private String migrationDBName;

    @Value("${spring.postgres.dbschema}")
    private String migrationDBSchema;

    @Value("${spring.postgres.username}")
    private String migrationDBUsername;

    @Value("${spring.postgres.password}")
    private String migrationDBPassword;

    @Bean(name = "migrationDataSource")
    public HikariDataSource migrationDataSource() {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName("org.postgresql.Driver");
        hikariConfig.setJdbcUrl("jdbc:postgresql://" + migrationDBHost + ":" + migrationDBPort + "/" + migrationDBName + "?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true");
        hikariConfig.setUsername(migrationDBUsername);
        hikariConfig.setPassword(migrationDBPassword);
        hikariConfig.setSchema(migrationDBSchema);

        return new HikariDataSource(hikariConfig);
    }
}

 

이번 코드는 Migration Destination DB인 PostgreSQL과의 연결을 위한 MigrationDBConfig 코드이다.

 

DataSource에는 생각보다 많은 내용이 포함되어 있어서 따로 포스팅을 한 뒤 이쯤에 다시 링크를 삽입할 계획이다. 간단히 설명하자면 DB와의 커넥션 정보를 담고 있는 오브젝트이다. DataSource 자체는 Connection과 관련된 메서드만을 포함하는 단순한 인터페이스이며 이를 구현한 구현체에는 SimpleDriverDataSource, SingleConnectionDriverDataSource, Apache Commons DBCP, HikariDataSource 등이 있다.

 

이중 HikariDataSource를 쓴 이유는 단순하다. 이게 제일 성능이 좋다.

 

과거 스프링부트 기본 구성을 보다가 hikaricp라는 익숙한 이름이 보여서 이게 뭔지 찾아봤었던 기억이 있다. (히카리는 자주 갔었던 홍대 스시야 이름이다.) HikariCP는 이름에서 볼 수 있듯 JDBC Connection Pool이다. 당시에는 Connection Pool인데 왜 이게 기본 구성일까 하는 생각도 들었었다.

 

HikariCP의 월등한 성능을 보여주는 지표. 출처: HikariCP 공식 깃헙

그리고 구글에 검색하자마자 공식 레포가 나왔고 이런 아름답고 자랑스러운 성능 지표 때문에 아직까지도 머리에 남아 있었다.

 

이후 DataSource를 내가 직접 선택해야 하는 때가 왔을 때, 클래스를 타고 들어가 보니, DataSource는 단순 Interface이고 여러 구현체가 존재한다는 사실을 알 수 있었다. DataSource에 대해서 더 찾아보니 Connection Pool 기능을 DataSource에서 담당한다는 것을 알게 됐고, 그렇다면 분명 HikariDataSource가 있을 것이라고 생각해 그대로 검색했다. 그리고 당연히 있었다.

 

어쨌든, 간단히 설정값들을 set 메소드로 추가해주고 DataSource 하나 반환해주면 설정은 완료된다.

 

이번 프로젝트는 JPA를 사용하기 때문에 DataSource만 구성해주면 된다. MyBatis 등의 SQL Mapper나 원시 JDBC를 사용해도 되지만.. 굳이 싶다. 만약 그렇게 되는 경우 해당 링크를 타고 가면 만들 수 있는 PlatformTransactionManager와 JdbcTemplate 등을 추가로 생성해줘야 한다.

 

설정을 완료하고, 애플리케이션을 실행시켰을 때 아래와 같은 로그들이 뜬다면 각각 설정이 잘 완료된 것이다.

 

HikariPool을 사용한다. 즉, HikariDataSource인 MigrationDBConfiguration이 정상적으로 적용됐음을 알 수 있다.
Mongo는 약간 요란하다. LegacyDBConfiguration도 정상적으로 적용됐음을 알 수 있다.

 

사용


이제 이렇게 얻은 DB instance와의 연결들을 가지고 실제로 데이터를 취급하는 방법을 알아보자. 간단히 Entity와 Repository로 이루어진다.

 

MongoDB (Spring-data-mongo)

@NoArgsConstructor
@Getter
@Document(collection = "Entity")
public class LegacyEntityExample {
    @Id
    private String id;
	
    ...
}

Mongo의 Document에 매핑될 Entity는 위와 같이 생성할 수 있다. Jpa와 비슷하지만 @Entity annotation이 없고, @Table 대신 @Document annotation이 있다. @Document는 @Table과 같이 매칭 할 document에 대한 정보를 기입해주면 된다.

 

주의할 점은 @Entity 어노테이션을 붙이면 안된다는 점이다. @Entity를 붙이게 되면 Jpa에 의해서 관리 대상으로 지목되며 EntityManager에도 등록되고 만약 ddl-auto: create 나 기타 위험한 설정들이 붙어있는 경우 해당 Entity의 이름과 같은 테이블이 삭제될 수 있다.

 

주의 또 주의하자.

 

public interface LegacyEntityRepository extends MongoRepository<Entity, String> {
	public Entity findbyId(String id);
}

Repository 인터페이스는 JpaRepository와 거의 유사하다. 

 

 

PostgreSQL (Spring-data-jpa)

@Getter
@Entity
@NoArgsConstructor
@Table(name = "Entity", schema = "public")
public class MigrationEntityExample{
    @Id
    private Long entityId;
    
    ...
}

Jpa는 사실 소스가 너무 많다. Entity는 다들 아는 방법으로 생성해주면 되며, 특이한 점은 postgres이기 때문에 schema를 따로 명시해줄 수 있다는 점이 되겠다.

 

public interface MigrationEntityRepository extends JpaRepository<Entity, Long> {

}

해당 Repository 에서는 Entity의 id type이 Long이기 때문에 Long으로 설정해준 것을 제외하면 크게 다르지 않다.

 

 

마치며


사실 두 개의 데이터베이스를 하나의 스프링 서버에 연결한다는 것에서 생각보다 고생을 많이 했다. 특히 그냥 yml로 설정할 때는 해당 설정이 왜 어떤 방식으로 어떻게 DB와 연결되는지 알 수 없기 때문에 그냥 다른 사람들이 적어둔 소스 대로만 적을 뿐이었다. 또한, 이처럼 두 타입의 데이터베이스를 한 번에 묶는 소스는 없어 yml 관련으로 찾으려 하니 정말 고통이었다.

 

하지만, 이렇게 Java 코드 기반의 Bean metadata를 등록하는 것은 개발자로하여금 설정을 더 직관적으로 볼 수 있게 만들어주기 때문에 한 번 제대로 정리해보고 싶었다. 메인 애플리케이션 개발 과정보다 환경 설정의 과정에 더 공을 들여 작성한 이유가 거기에 있다.

 

이 글도 묻힐 수 있겠지만, 누군가는 필수 검색어로 추가해 이 소스를 찾을 수 있길 바라며... 이만 마친다..

 

 

참고자료


Spring-data-mongo 공식문서: https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/ 

 

Spring Data MongoDB - Reference Documentation

abs, acos, acosh, add (* via plus), asin, asin, atan, atan2, atanh, ceil, cos, cosh, derivative, divide, exp, floor, integral, ln, log, log10, mod, multiply, pow, round, sqrt, subtract (* via minus), sin, sinh, tan, tanh, trunc

docs.spring.io

거의 유일했던 한글 소스: https://frogand.tistory.com/132

 

[Spring JPA] Multiple Databases 다중 DB 연결하기

- Spring Data JPA - Spring Boot application.yml spring: datasource: hikari: bootdb1: driver-class-name: [driver-name] jdbc-url: [url] username: [username] password: [password] bootdb2: driver-class-..

frogand.tistory.com

언급됐던 HikariCP: https://github.com/brettwooldridge/HikariCP

 

GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last.

光 HikariCP・A solid, high-performance, JDBC connection pool at last. - GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last.

github.com

MongoClient 세팅에 도움이 된 글: https://stackoverflow.com/questions/70822500/autowiring-mongoclient-and-mongoclientsettings-without-explicitly-specifying-a-c

 

Autowiring MongoClient and MongoClientSettings without explicitly specifying a Connection String

I am upgrading the MongoDB driver which requires moving away from the older MongoClientOptions to the newer MongoClientSettings. In the older implementation, the following configuration was used wi...

stackoverflow.com

댓글