기술 블로그 출처
https://techblog.lycorp.co.jp/ko/experience-in-migrating-order-db-on-ecommerce-platform
DB 모델 재정의
DB 이관 전에 MySQL에서 사용할 DB 모델을 재정의한 이유는 기존 Oracle 모델에서 조회 성능이 나오지 않았기 때문이다. 주문 데이터 조회 시 17개의 테이블을 조인해야 했으며, 이로 인해 부하가 발생하여 주문당 최대 5초 이상 소요되었다. 새로운 DB 모델을 정의할 때 주된 관심사는 조인을 줄이는 것이다. 이를 해결하기 위한 방법으로 일부 테이블을 JSON 문자열로 저장해 역정규화하는 안과, 두번째로 정규화 테이블과 비정규화 테이블 두 쌍을 운용하는 방법이다.
- JSON 문자열로 저장하여 역정규화 진행
- One-to-Many 관계가 많아 컬럼으로 역정규화하기 어려웠고, 따라서 JSON Object 또는 JSON Array로 역정규화하는 아이디어 제안
- JPA를 사용하므로 코드 작성에 큰 어려움은 없지만, JSON 문자열로 저장한 필드를 검색해야 할 경우 테이블 구조를 다시 변경해야 함
- 검색용 모델과 조회용 모델을 분리하여 운용
- 단점: 2개의 모델을 관리해야 하므로 코드 양과 DB에 저장되는 데이터 양이 늘어남
- 장점: 검색이 유연하고 조회 성능이 뛰어남
1번 방법이 2번 방법보다 읽기, 쓰기 성능이 떨어졌기 때문에 2번 방법을 선택했다. 성능을 테스트할 때 JPA의 Batch Insert를 사용했음에도 불구하고, 데이터가 더 많은 두 번째 안이 더 느린 것으로 나타났다. Pinpoint 등을 사용하여 원인을 분석한 결과, JPA의 변경 감지와 Jackson의 특성 때문인 것으로 확인됐다.
- Jackson 호출 횟수: 1번 방법에서 예상보다 많은 Jackson 호출이 있었는데, 이는 JPA가 AttributeConverter를 사용하는 엔티티를 저장할 때 변경을 감지하기 위해 항상 직렬화와 역직렬화를 호출하기 때문이다.
- Jackson 호출 소요 시간: 두 방법 모두 Jackson 호출 성능에는 거의 차이가 없었습니다. 이에 따라 큰 객체를 작은 객체로 나눠 여러 번 직렬화하는 첫 번째 안보다 큰 객체로 한 번만 직렬화하는 두 번째 안이 더 좋은 성능을 보여줬다.
따라서, Jackson 호출 횟수 및 수행 시간의 영향으로 두번째 방법이 더 좋은 성능을 보여줬기 때문에 정규화 테이블과 비정규화 테이블 두 쌍을 운용하는 두 번째 안을 선택했다.
퍼시스턴스 레이어 개발
테이블 운용 방법을 결정한 후 퍼시스턴스 레이어를 개발하는 데에는 두 가지 주요 문제가 있었다. 첫째는 JPA를 사용할 때 데이터를 업데이트하려면 먼저 데이터를 읽어와야 한다는 점으로, 이는 정규화 테이블에 대량의 조인을 발생시킬 수 있다는 것이었다. 둘째는 정규화 테이블과 비정규화 테이블 간의 일관성이 깨질 수 있다는 것이었다.
해결 방법
- 명세 객체 사용: 서비스 레이어와 퍼시스턴스 레이어 간에 도메인 객체 대신 별도의 명세 객체를 사용하여 통신하도록 설계했다. 명세 객체는 업데이트에 필요한 정보만을 갖고 있어서 퍼시스턴스 레이어가 정규화된 테이블 전체를 읽어오지 않고도 작업을 수행할 수 있다.
- 애그리게이션 활용: 정규화 테이블에 대응하는 JPA 엔티티의 연관 관계를 데이터가 변경되는 단위로 쪼개어 애그리게이션을 만들었다. 이를 통해 항상 페치 조인으로 데이터를 읽어와 N+1 쿼리 문제를 방지하고 부분적인 데이터 업데이트를 가능하게 했다.
- 도메인 객체 생성과 명세 반영의 분리: 퍼시스턴스 레이어 안에서는 명세를 받고 이를 정규화 테이블을 통해 반영하고, 도메인 객체는 비정규화 테이블을 통해 생성하도록 설계했다. 이를 통해 두 테이블 간의 일관성을 유지하고 코드 일관성을 확보했다.
이러한 방법을 통해 대량의 조인을 방지, 두 테이블 간의 일관성 유지, 퍼시스턴스 레이어의 효율성 향상을 달성했다.
동시성 이슈
주문 프로세스에서 각 상품을 독립적이고 순차적으로 처리하던 중, 주문 전체를 JSON 문자열로 저장하는 과정에서 발생한 동시성 이슈로 인해 데이터 정합성 문제가 발생했다. 정규화 테이블은 부분적으로 업데이트가 가능하여 문제가 없으나, 비정규화 테이블에는 새로운 JSON 문자열로 치환되는 방식으로 저장되어 서로의 변경 사항을 인식하지 못하면서 데이터가 손실되었다.
비정형 데이터 구조를 사용하는 경험이 부족한 저희 팀이 처음 겪은 데이터 정합성 문제를 해결하기 위해 Redis를 이용한 분산 락을 도입했다. 하지만 분산 락과 코드 구조 간의 불일치로 인해 재진입 문제가 발생했다. 이를 해결하기 위해 Spring의 Transactional과 유사한 인터페이스를 락에 적용하여 재진입을 허용하는 분산 락을 구현했다. 이로써 비슷한 문제에 대응할 수 있는 유연한 분산 락을 만들었고, 결과적으로 문제를 해결하는 데 도움이 됐다.
배포
배포 단계
- 신규 모델 검증: 비동기 방식으로 MySQL에 데이터를 적재하고 검증
- 이중 쓰기: 동기 방식으로 Oracle과 MySQL에 데이터를 적재하며, 마이그레이션과 비즈니스 프로세스 간 충돌 방지
- MySQL로 읽기 전환: MySQL에도 실시간으로 최신 데이터가 저장되어 MySQL로 읽기를 전환하고 이중 쓰기 로직 제거
- Oracle 수정 제거: 수정 단계부터 Oracle를 제거하여 배포 호환성 보장
- 쓰기에서 Oracle 제거: Oracle 의존성을 완전히 제거하여 MySQL만으로 운용