마이그레이션이란
데이터,애플리케이션 운영체제 혹은 전체 시스템 환경을 한 플랫폼에서 다른 플랫폼으로 안전하게 옮기는 과정이다
따라서 Oracle에서 MySQL로 전환하는것도 마이그레이션에 해당한다
마이그레이션을 하는 이유
스마트 스토어 플랫폼내에서 여러파트가 OracleDBMS를 사용했고 비즈니스 성장에 따라 리소스 경합 즉 컴퓨터 시스템에서 여러 프로세스, 스레드 또는 장치가 CPU, 메모리, 디스크, 네트워크 등 제한된 공유 자원을 동시에 사용하려고 할 때 발생하는 충돌과 경쟁 현상이 발생하였기 때문에 서비스성능에 불안정을 만들었고 또한 Oracle은 유로이므로 확장시 막대한 라이선스 비용이 필요하기때문에 스마트스토어 회원 파트는 운영의 효율성을 챙기고 비용을 절감하기 위해 오픈소스인 MySQL로 DBMS 마이그레이션을 결정했다
마이그레이션 준비 및 전략수립
기존 오라클 DBMS에서는 JPA와 MyBatis를 사용하고 있었다 마이그레이션 할 DB가 회원파트이기 때문에 타부서 시스템과 광번위하게 연결되있어서 서비스를 중단하고 옮기는 것이 불가능했고 따라서 무중단 배포를 할수밖에 없었는데 사소한 기능 버그가아닌DB를 전환하고 만날수 있는 치명적인 성능저하나 서비스 장애가 발생하는 경우 재배포만으로 해결할수 없어서 빠른 롤백능력도 확보해야했다
전환전략으로 이중쓰기 선택
Kubernetes 환경은 컨테이너화된 애플리케이션의 배포, 확장, 관리를 자동화하는 오픈소스 오케스트레이션 플랫폼이라고 한다 이환경에서 MyBatis 및 Spring JPA를 사용하는 기존 Oracle DB 기반 서비스를 MySQL로 무중단 전환하고 롤백 가능성을 확보하기 위해 이중 쓰기(dual write) 기법을 선택했다 이중쓰기란 모든 쓰기 트랜잭션을 기존 데이터 베이스와 새 데이터베이스에 동시에 반영하는 기법으로 신규 시스템의 안정성을 검증하는 동안 두 DB가 완벽라게 동기화 된 상태를 유지하는것을 보장해서 데이터 손실이나 서비스 중단없이 안전하게 신규환경으로 전환할수 있게하는 안전 장치다
전환과정
1.전환전 단계
구버전 애플리케이션을 모든 읽기와쓰기 트래픽을 오라클에서 처리하는데 Create,Update,Delete작업시에만 백그라운드에서 MySQL에도 이증쓰기를 실행한다
2.데이터 마이그레이션
신버전 배포전에 오라클의 전체데이터를 마이SQL로 바꿔서 정합성을 맞춘다
정합성 : 데이테 베이스내의 데이터가 서로 모순없이 일관되게 일치하는 상태
3.전환후 단계
신버젼에서는 모든 읽기와 쓰기 트래픽을 마이SQL에서 처리하며 Create,Update,Delete 작업시 백그라운드에서 오라클에도 이중쓰기를 실행한다
위의 세가지 구조를 통해 데이터의 정합성을 유지하면서 서비스 중단없이 배포가 가능해지며 오라클에 이중쓰기가 계속 되므로 롤백이 필요한 경우 별도의 데이터 복구과정 없이 롤백할수 있다
기술적 도전과 해결과정
JPA이중쓰기
JPA : 자바 애플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스 표준(ORM 기술)으로 SQL을 직접 작성하는 대신 객체를 통해 데이터베이스를 다루어 개발 생산성과 유지보수성을 높이고, Hibernate 등의 구현체를 통해 동작한다 스프링 부트에서는 주로 Spring Data JPA를 함께 사용하여 DB 연동을 자동화한다
datasource-proxy 라이브러리란 Java JDBC DataSource를 프록시(Proxy)로 감싸 애플리케이션과 데이터베이스 사이의 쿼리 실행을 가로채고 모니터링, 로깅, 성능 측정 등을 수행하는 라이브러리다
datasource-proxy 라이브러리를 활용해 Oracle에서 수행되는 쿼리를 가져온 뒤 별도의 MySQL DataSource로 해당 쿼리를 실행하도록 JPA용 DataSource를 구성후 빈등록 시점에서 DataSource구현을 위한 설정을 해주었다
따라서 이렇게 설정하면 EntityManager에서 Oracle DB로 쿼리가 데이터를 DB에 반영될때 MySQL DB에도 쿼리가 수행된다
문제 해결하기
한트랜잭션 내에서 여러쿼리가 수행될때 오류가 발생하면 두 DB간의 정합성이 유지 되는가?
이중쓰기 데이터소스에서 두 DB의 데이터소스를 참조하더라도 실제 사용되는 메인 DB는 오라클 데이터 소스이다 마이SQL 데이터 소스는 트랜잭션으로 관리되지 않는다 따라서 롤백을 실행할때 두 DB의 정합성이 깨지기 때문에 이중쓰기를 할때 트랜잭션 대응을 별도로 만들어야한다
이중쓰기를 구현하는 시점에서 오라클과 마이SQL간 데이터 정합성이 맞지 않고, 서비스내 JPA의 모든 오라클 CUD가 마이SQL에서 제대로 동작하는 지 검증되지 않았고 테이블 구성이나 환경설정이 최적화 되지 않았기 때문에 ChainedTransactionManager를 사용하거나 분산 트랜잭션으로 MySQL 쿼리를 트랜잭션에 포함시키면 안된다
실제 읽기 전환이 이루어지기전에 다양한 이유로 마이SQL쿼리가 실패할수 있기때문에 트랜잭션에 따라 최소한의 단위로 진행하되 실패시 트랜잭션이 롤백되지 않고 무시되어야한다 따라서 MySQL 쿼리만 실패하는 케이스를 넘어가고 주기적으로 마이그레이션과 검증을 반복하며 원인을 찾아서 제거 하는 방식을 선택했다오라클에서 쿼리수행에 영향을 주지 않는 확실한 방법은 트랜잭션 중 수행되는 쿼리를 모았다가 커밋한후 한번에 마이SQL에서 실행하는것이다
엔티티설정하기
오라클에서는 Primary Key를 만들기 위해서는 시퀀스를 사용했으나 마이SQL에서는 시퀀스가 없으므로 auto increment를 사용한다이렇게 변경하면 마이SQL INSERT문은 PK가 존재하지 않기때문에 읽기로 변경후 마이SQL에서 오라클로 이중쓰기를 진행할때 쿼리를 그대로 사용할수 없다 따라서 마이SQL INSERT 쿼리 수행시 generated key정보를 가져와서 PK를 채워서 실행할수 있다
Generated Key(생성된 키)란 데이터베이스 테이블에서 레코드(row)를 고유하게 식별하기 위해 애플리케이션이 아닌 데이터베이스(DBMS)가 자동으로 생성하는 기본 키(Primary Key) 값을 의미한다
MyBatis 이중쓰기
문제점기존 오라클 데이터소스와 신규 마이SQL 데이터소스를 처리하는 MyBatis인터페이스를 각각 분리해서 호출하는 방식은 코드변경의 범위가 지나치게 넒음 따라서 모든 로직에 MyBatis 사용시 기존 레거시 코드 전체를 수정해야함 이는 휴먼에러 발생가능성과 개발기간이 크게 늘어나는 일이되어 위험성이 높음
MyBatis는 @Mapper 어노테이션이 붙은 인터페이스가 XML등에 정의된 쿼리아이디와 쿼리를 참조하는 구조다
설정과정
- SqlSession은 Configuration 객체참조
- Configuration 객체 안에는 XML 등에 정의된 쿼리가 QueryId와 함께 MappedStatement 객체로 변환됨
- @Mapper 애너테이션이 붙은 MyBatis용 인터페이스는 MapperProxy 빈으로 등록되고, mybatisRepository.findById() 등의 메서드 호출은 MapperProxy 객체의 invoke 함수가 호출되는 구조
- invoke 함수 ->프로그래밍에서 함수, 메서드, 명령 또는 프로그램을 간접적으로 호출하거나 시동하는 기능
- MapperProxy의 invoke는 MapperMethod의 execute를 실행한다 MapperMethod의 execute는 쿼리 타입에 맞게 SqlSession의 메서드를 실행한다
즉 XML에 정의된 SQL은 MappedStatement라는 객체가 되고, SqlSession은 MyBatis 설정 시 주입된 DataSource를 참조하면서 이 MappedStatement 객체를 보유했다가 필요할 때 실행한다
구현방법
쿼리문법차이를 고려해서 마이SQL 문법에 맞는 SQL로 작성된 MyBatis XML을 하나더 만들어한다 오라클과 MySQL 간에 문법 차이가 있으므로 모든 MyBatis XML 쿼리를 MySQL 문법에 맞게 재작성해 위 구조로 MyBatis 이중 쓰기를 실행한다
쿼리 및 DBMS 성능검증
오라클에서 안전하던 게 MySQL에서도 안전하다고 가정하면 안 된다 이중 쓰기 동안 실제 트래픽을 MySQL에 흘려보내고, CPU/메모리/커넥션 같은 지표를 촘촘히 측정해서 과부하가 생길 쿼리나 구조를 미리 찾아 성능 저하와 장애를 예방해야 한다
성능검증 전략
타부서와 연관된 시스템에 영향을 주지 않고 성능검증 이라는 목표를 달성하기 위해 이미 수행중인 이중쓰기 로직을 유지하면서 내부 서비스로직에서 발생하는 읽기 트래픽만 새로운 마이SQL DB로 복제해서 호출한다
- 영속성 -> 프로그램 종료 후에도 데이터가 사라지지 않고 데이터베이스나 파일 시스템 등에 지속적으로 저장되는 성질
- 직렬화 -> 메모리에 존재하는 객체나 데이터 구조를 네트워크 전송이나 파일 저장이 가능하도록 연속적인 바이트 또는 텍스트 스트림(JSON, XML 등)으로 변환하는 과정
MyBatis나 JPA 같은 영속성 프레임워크에서는 Read(조회) 메서드를 호출할 때, SQL로 변환되기 전 단계에서는 보통 int, long, String 같은 원시 타입이나 DTO처럼 직렬화 가능한 객체를 매개변수로 전달한다 이 특성을 활용해 기존 Oracle로 향하는 Repository 계층의 Read 메서드가 호출되는 순간을 기준으로, 메서드 이름과 매개변수 값을 캡처했다 동시에 해당 호출의 실행 시간을 함께 측정해 “어떤 조회가, 어떤 입력값으로, 얼마나 느렸는지”를 한 번에 추적할 수 있도록 했다 수집한 데이터는 JSON 형태로 직렬화해 저장했다
Oracle에서 실제로 발생한 조회 호출 정보를 Kafka로 보내고, 별도 Consumer가 그 정보를 받아 MySQL Repository의 같은 조회 메서드를 똑같이 호출해서 “운영 트래픽과 동일한 읽기 부하”를 MySQL에서 재현한 방식이다
성능검증을 통하여 성능이 좋지 않은 쿼리를 수정하고 인덱스 추가등 을 실행해 안정적인 성능을 확보했다
데이터 정합성 검증
이중쓰기로 실시간 동기화는 성공했비만 쓰기에 문제가 있어 데이터가 다르게 쓰이고 있지는 않은지 두 DBMS 간의 데이터 일치 여부를 검증했다
Airflow를 사용해서 정해진 시간마다 정해진 순서대로 자동으로 실행되는 정합성 검증 파이프라인(워크플로)을 만들었다
Airflow 파이프라인이 주기적으로
- 기존 Oracle DB에서 주요 테이블 데이터 추출
- 신규 MySQL DB에서 같은 테이블 데이터 추출
- 두 데이터를 Hive 데이터 웨어하우스에 모아서(통합해서) 저장
이렇게 비교하기 좋은 환경을 만든 것이다
데이터가 크면 DB끼리 직접 비교하는 게 느리고 부담이 크다
그래서 Hive에서 분산 처리(여러 노드가 나눠서 계산)로 비교 쿼리를 실행해 빠르게 검증했다
이 과정에서 발견된 정합성이 일치하지 않는 건에 대애서 분석 및 수정을 거쳐서 최종적으로 두 데이터 베이스간의 정합성이 100이 되도록 조치했다 이러한 검증 단계는 기존 시스템을 종료(Down)하고 신규 시스템으로 전환(Open)하여 실제 운영 환경을 적용하는 최종 단계(컷오버)를 위한 결정적인 안전장치가 되었다
마이그레이션 후 아키텍쳐 및 검증
트러블슈팅 및 기타 고려사항
Index Merge는 MySQL에서 WHERE 절에 여러 인덱스 칼럼이 OR 조건으로 연결되는 경우, 각 인덱스를 독립적으로 검색한 후 결과를 합치는 방법이다 그러나 조건이 조금만 복잡해지면 최적화하지 못하고 풀스캔을 사용한다 따라서 풀스캔이 사용되는경우 UNION으로 쿼리를 변경했다
마이그레이션 배치성능이슈
테이블의 크기가 충분할때는 하나의 PK 페이징은 인덱스로 필요한 구간만 읽고 LIMIT만큼 찾으면 멈추는 방식(범위 스캔 + 조기 종료)으로 최적화될 수 있다 반면 여러개의 PK 페이징은 서브쿼리 정렬과 OR 조건 때문에 인덱스 시작점을 잡기 어려워, 옵티마이저가 최적화하지 못하고 전체 정렬 후 100건만 추출하는 비효율적인 실행방법으로 동작한다
이것을 해결하려면 PK순서로 조건을 만족하는 상위 100건을 조회할수 있도록 쿼리 튜닝이 필요하다 OR 조건 사용으로 INDEX FULL SCAN이 발생할 수 있으므로 UNION ALL 구문으로 변경한다 이렇게 변경하면 인덱스를 탄 두번의 TOP-N의 조회후 최종적으로 페이지 사이즈만큼 행을 가져온다
MySQL의 HikariCP 설정
MySQL을 쓰는 스프링 서버에서 HikariCP는 DB 커넥션을 미리 여러 개 만들어 풀(pool)에 보관했다가, 요청이 오면 꺼내 쓰고 다시 돌려놓는 커넥션 풀이다 매 요청마다 새로 연결/해제하는 비용을 줄여서 성능과 안정성을 챙기는 역할을 한다
Statement는 정적인 쿼리를 실행할 때, PreparedStatement는 동적인 쿼리를 실행할 때 주로 사용한다 JDBC 인터페이스로 DBMS(MySQL)의 PreparedStatement를 사용하기때문에 JDBC 없이 MySQL만 사용해도 PreparedStatement를 사용할 수 있다
dataSource.cachePrepStmts=true
dataSource.prepStmtCacheSize=250
dataSource.prepStmtCacheSqlLimit=2048
dataSource.useServerPrepStmts=true
dataSource.useLocalSessionState=true
dataSource.rewriteBatchedStatements=true
dataSource.cacheResultSetMetadata=true
dataSource.cacheServerConfiguration=true
dataSource.elideSetAutoCommits=true
dataSource.maintainTimeStats=false
이 설정 묶음은 HikariCP 풀 자체라기보다, HikariCP를 통해 생성되는 MySQL 커넥션에 드라이버 성능 옵션을 주입해 반복 쿼리와 대량 처리에서 병목이 되는 비용을 줄이는 튜닝이다. 특히 PreparedStatement 캐시와 배치 재작(rewriteBatchedStatements)은 체감 효과가 커서 MySQL 환경에서 자주 함께 세팅하는 편이다.
Oracle 시퀀스와 MySQL auto increment
오라클 시퀀스를 MySQL auto increment로 변경하면서 식별자 생성 시점의 차이가 발생해 연관관계 객체 저장 로직을 수정해야했다 기존 오라클 환경에서는 시퀀스 기반으로 식별자가 insert 이전에 생성되서 연관관계 설정과 저장로직이 자연스럽게 실행되었다
그러나 MySQL에서는 insert이후 생성되기 때문에 로직을 그대로 사용하면 연관관계를 설정했을때 아이디가 존재하지 않기때문에 의도치 않은 일이 발생할수 있다 따라서 DB를 반영하는 시점조정이나 저장순서 변경이 필요했고 식별자에 의존하던 로직을 정리하고 연관관계의 주인 설정을 명확히 하는등 코드의 수정을 진행했다 로직 수정이 불가능한경우 데이터를 식별하기 위해 고유한 번호(코드)를 새로 부여하는 작업을 진행할 채번용 MySQL테이블을 만들어서 아이디를 채번하도록 수정했다
성공적인 이관작업의 결과
이관 성공 → 오라클 접속 감소 → 오라클이 쓰던 작업 메모리(PGA) 감소 → 서버 메모리 여유 증가 → 스왑 사용 감소 → 시스템이 더 안정적/성능 유리의 흐름을 가지게 되었다 또한 남는 자원 덕분에 공용 장비는 더 안정적이 됐고, 우리는 공용 장비 눈치/제약 없이 파드를 늘려 확장할 수 있는 기반을 만들었다
그리고 성능 모니터링을 통해 시스템 최적화를 수행한 결과 서비스의 응답 지연시간이 개선되는 효과가 있었다
참고블로그
'🐢 꼬부기 LV.1 | 개념•기초 > 💧물대포(핵심개념)' 카테고리의 다른 글
| SQL 자격검정 실전문제 과목2 문제풀이81~98 (0) | 2026.02.20 |
|---|---|
| SQL 자격검정 실전문제 과목 2 문제풀이 66~80 (0) | 2026.02.18 |
| SQL 자격검정 실전문제 과목 2 문제풀이 51~65 (0) | 2026.02.16 |
| VS 코드 설치하기 (0) | 2026.02.16 |
| SQL 자격검정 실전문제 과목2 제 1장 오답노트 30~50 (0) | 2026.02.15 |