본문 바로가기

개발기술/RDB

DB 마이그레이션

DB 마이그레이션

기존 데이터베이스를 새로운 DB로 이전하는 전체 과정(전략 + 기술 + 실행) 

 

DB 변경은 단순히 “데이터만 옮기는 것”이 아니라:

  • 스키마(SQL 문법) 차이 조정
  • 애플리케이션 코드 수정
  • 트랜잭션 처리 방식 변화
  • 성능 설정 변경
  • 데이터 정합성 검증
  • 장애 대비 롤백 계획

 

DB 마이그레이션의 4가지 방법론

1) Big Bang 방식 — "한 번에 갈아끼우기"

서비스 중단
↓
dump → restore
↓
yml DB 주소 변경
↓
서비스 재실행

 

✔ 장점: 단순, 빠름
❌ 단점: 다운타임 있음, 실패하면 대형 사고
🎯 소규모 서비스에서 가장 많이 쓰임

 

2) Trickle Migration (점진적 전환)

구 DB ←→ 신 DB
   ↑       ↑
동시 저장, 동기화

 

방법

  • 일정 기간 두 DB 모두에 저장 : 자바 코드에서 save 두 번 
  • 검증 완료 후 신 DB만 사용
  • 실패 시 롤백 가능

 

장단점

  • ✔ 무중단
  • ✔ 안전
  • ❌ 설정 복잡도 높음 → 개념 이해 필요
  • 🎯 중대규모 서비스에서 많이 사용

 

방법론 : 멀티디비설정

 

 

📁 전체 구조

  프로젝트
  ├── application.yml          # 두 DB 연결 정보
  ├── config/
  │   ├── MariaDbConfig.java   # MariaDB 설정
  │   └── MssqlDbConfig.java   # MSSQL 설정
  ├── db/
  │   ├── mariadb/             # MariaDB 전용
  │   │   ├── repository/
  │   │   └── entity/ (공통 도메인 사용)
  │   └── mssql/               # MSSQL 전용
  │       ├── repository/
  │       └── entity/
  └── service/
      ├── NexReal3DStatisticsService.java  # MariaDB 저장
      ├── MssqlStatisticsService.java      # MSSQL 저장
      └── ThreadExecutorService.java       # 양쪽 호출

  ---
  1️⃣ application.yml (두 DB 연결)

  spring:
    datasource:
      # MariaDB 설정
      maria:
        hikari:
          pool-name: master
          maximum-pool-size: 50

      # MSSQL 설정
      mssql:
        driver-class-name: cohttp://m.microsoft.sqlserver.jdbc.SQLServerDriver
        hikari:
          pool-name: mssql
          maximum-pool-size: 50

  ---
  2️⃣ MariaDbConfig.java (MariaDB 전용)

  @Configuration
  @EnableJpaRepositories(
      basePackages = "dains.platforhttp://m.batch.db.mariadb.repository",  // ← 패키지 분리!
      entityManagerFactoryRef = "mariaEntityManagerFactory",
      transactionManagerRef = "mariaTransactionManager"
  )
  public class MariaDbConfig {

      @Bean
      @ConfigurationProperties("spring.datasource.maria")
      public DataSource mariaDataSource() {
          return DataSourceBuilder.create().build();
      }

      @Bean
      public LocalContainerEntityManagerFactoryBean mariaEntityManagerFactory(
              EntityManagerFactoryBuilder builder) {

          Map<String, Object> properties = new HashMap<>();
          properties.put("hibernate.dialect", "org.hibernate.dialect.MariaDBDialect");

          return builder
              .dataSource(mariaDataSource())
              .packages("dains.platforhttp://m.common.domain")  // ← Entity 위치
              .persistenceUnit("maria")
              .properties(properties)
              .build();
      }

      @Bean
      public PlatformTransactionManager mariaTransactionManager(
              @Qualifier("mariaEntityManagerFactory") EntityManagerFactory emf) {
          return new JpaTransactionManager(emf);
      }
  }

  ---
  3️⃣ MssqlDbConfig.java (MSSQL 전용)

  @Configuration
  @EnableJpaRepositories(
      basePackages = "dains.platforhttp://m.batch.db.mssql.repository",   // ← 다른 패키지!
      entityManagerFactoryRef = "mssqlEntityManagerFactory",
      transactionManagerRef = "mssqlTransactionManager"
  )
  public class MssqlDbConfig {

      @Bean
      @ConfigurationProperties("spring.datasource.mssql")
      public DataSource mssqlDataSource() {
          return DataSourceBuilder.create().build();
      }

      @Bean
      public LocalContainerEntityManagerFactoryBean mssqlEntityManagerFactory(
              EntityManagerFactoryBuilder builder) {

          Map<String, Object> properties = new HashMap<>();
          properties.put("hibernate.dialect", "org.hibernate.dialect.SQLServerDialect");

          return builder
              .dataSource(mssqlDataSource())
              .packages("dains.platforhttp://m.batch.db.mssql.entity")  // ← 별도 Entity
              .persistenceUnit("mssql")
              .properties(properties)
              .build();
      }

      @Bean
      public PlatformTransactionManager mssqlTransactionManager(
              @Qualifier("mssqlEntityManagerFactory") EntityManagerFactory emf) {
          return new JpaTransactionManager(emf);
      }
  }

  ---
  4️⃣ Repository 계층 (패키지 분리)

  MariaDB Repository

  package dains.platforhttp://m.batch.db.mariadb.repository;

  // MariaDB용 Repository
  public interface Count15SecStatsRepository 
      extends JpaRepository<Count15SecStats, Count15SecStatsPk> {
  }

  MSSQL Repository

  package dains.platforhttp://m.batch.db.mssql.repository;

  // MSSQL용 Repository
  public interface TUCountRepository 
      extends JpaRepository<TUCount, Long> {
  }

  ---
  5️⃣ Service 계층 (트랜잭션 분리)

  MariaDB Service

  @Service
  @RequiredArgsConstructor
  @Transactional(transactionManager = "mariaTransactionManager", readOnly = true)
  public class NexReal3DStatisticsService {

      private final Count15SecStatsRepository count15SecStatsRepository;  // MariaDB

      @Transactional(transactionManager = "mariaTransactionManager")
      public void saveNexReal3D15SecStatistics(Integer cameraNo, NexRealDto dto) {

          // MariaDB에 저장
          Count15SecStats stats = Count15SecStats.builder()
              .cameraNo(cameraNo)
              .totalCount(1)
              .build();

          count15SecStatsRepository.save(stats);
      }
  }

  MSSQL Service

  @Service
  @RequiredArgsConstructor
  @Transactional(transactionManager = "mssqlTransactionManager", readOnly = true)
  public class MssqlStatisticsService {

      private final TUCountRepository tuCountRepository;  // MSSQL

      @Transactional(transactionManager = "mssqlTransactionManager")
      public void saveMssqlStatistics(NexRealDto dto) {

          // MSSQL에 저장
          TUCount tuCount = TUCount.builder()
              .cameraId(dto.getCameraId())
              .cnt("1")
              .build();

          tuCountRepository.save(tuCount);
      }
  }

  ---
  6️⃣ 양쪽 DB 동시 저장 (핵심!)

  @Service
  @RequiredArgsConstructor
  public class ThreadExecutorService {

      private final NexReal3DStatisticsService nexReal3DStatisticsService;  // MariaDB
      private final MssqlStatisticsService mssqlStatisticsService;          // MSSQL

      public void saveNexReal3D15SecStatistics(Integer cameraNo, NexRealDto dto) {

          // MariaDB 저장
          handleAsync(() ->
              nexReal3DStatisticsService.saveNexReal3D15SecStatistics(cameraNo, dto)
          );

          // MSSQL 저장 (동시에!)
          handleAsync(() ->
              mssqlStatisticsService.saveMssqlStatistics(dto)
          );
      }

      private void handleAsync(Runnable task) {
          try {
              task.run();
          } catch (Exception e) {
              log.error("Error: {}", e.getMessage());
          }
      }
  }

  ---
  7️⃣ Scheduler에서 호출

  @Component
  @RequiredArgsConstructor
  public class NexRealScheduler {

      private final ThreadExecutorService threadExecutorService;

      @Scheduled(fixedRate = 10000)
      public void process() {

          NexRealDto dto = getDataFromRedis();

          // 한 번 호출로 양쪽 DB에 저장!
          threadExecutorService.saveNexReal3D15SecStatistics(cameraNo, dto);
      }
  }

  ---
  핵심 포인트 정리

  ✅ Config 분리

  MariaDbConfig → basePackages = "db.mariadb.repository"
  MssqlDbConfig  → basePackages = "db.mssql.repository"

  ✅ TransactionManager 분리

  @Transactional(transactionManager = "mariaTransactionManager")
  @Transactional(transactionManager = "mssqlTransactionManager")

  ✅ Repository 패키지 분리

  db.mariadb.repository.Count15SecStatsRepository
  db.mssql.repository.TUCountRepository

  ✅ 양쪽 동시 저장

  nexReal3DStatisticsService.save();  // MariaDB
  mssqlStatisticsService.save();      // MSSQL

  이게 Trickle Migration의 핵심입니다!
  두 DB에 동시에 저장하면서 검증하고, 안정화되면 MSSQL 코드를 제거하는 방식입니다.

 

3) CDC (Change Data Capture) 기반 Migration

기존 DB에서 발생하는 INSERT/UPDATE/DELETE를 실시간으로 읽어서 새로운 DB에 반영하는 기술

[구 DB] → 로그(Change Data Capture) → [신 DB]

 

  • 구DB에서 binlog, redo log를 읽어서  새로운 DB로 실시간 동기화
  • 먼저 기존 DB → 신규 DB로 전체 데이터를 한 번 복사함. Full copy가 끝난 시점부터는 다음 변경을 CDC(binlog, redo log, WAL 등) 로 캡처해서 지속적으로 신규 DB에 반영함

 

장단점 비교

  • 애플리케이션 변경 없이 가능 :자바 코드에서 save 두 번 할 필요 없음
  • ✔ 가장 안전하고 정교함
  • ❌ 구축 난이도 + 비용
  • 🎯 대규모, 금융/대기업에서 선호