개요
이전 포스팅에서 ETL 데이터 파이프라인을 구축했습니다.
https://growth-coder.tistory.com/359
식당 추천 시스템 개발기 #4 - ETL 데이터 파이프라인 아키텍처
개요"현재 자신의 상황에 맞는 식당 추천 시스템"을 개발하기 위해 지금까지 식당, 리뷰 크롤링 및 기본적인 벡터 기반 유사도 검색에 대해 알아보았습니다. 이번에는 작성한 크롤링 코드를 기
growth-coder.tistory.com
이번에는 데이터베이스를 역할에 따라 분리하게 되면서 아키텍처에 약간의 변화가 생겼습니다.
우선 식당 유사도 검색을 위한 Vector DB와 식당 메타데이터 저장을 위한 식당 DB를 분리하게 되면서 해결해야 할 문제가 생겼습니다.
바로 데이터 정합성 문제입니다.
우선 저희 프로젝트에서 사용하는 유사도 검색 API의 흐름은 다음과 같습니다.
- 사용자가 자연어 쿼리를 전송
- LLM API를 활용해서 분석 및 임베딩 벡터 생성
- 벡터 유사도 검색해서 식당 id 리스트 조회
- 식당 서버의 식당 메타데이터 조회 API 호출
- 식당 메타데이터 리스트 반환
여기서 중요한 점은 벡터 DB에 저장된 식당 메타데이터 정보는 반드시 식당 DB에 저장되어 있어야 합니다.
그렇지 않으면 유사도 검색으로 조회한 식당의 메타 데이터를 조회하지 못 해 장애가 발생하게 됩니다.
그런데 기존 아키텍처의 경우 정합성을 지키기 위한 수단이 마련되어 있지 않았습니다.
우선 기존에는 아래처럼 Lambda 함수 하나가 식당 DB와 Vector DB에 저장해서 데이터를 저장하고 있었습니다.

그런데 간혹 Vector DB 저장은 성공했지만 식당 DB 저장은 제약 조건을 위배하면서 실패하는 경우가 있었습니다.
트랜잭션을 통해 원자성을 확보했다면 정합성을 지킬 수 있었겠지만 서로 다른 두 DB 간 트랜잭션은 불가능했기 때문에 데이터 정합성이 지켜지지 않았습니다.
그래서 데이터 정합성을 지키기 위해 Outbox Transaction Pattern을 적용하게 되었습니다.
Transaction Outbox Pattern이란?
Transaction Outbox Pattern이란 애플리케이션이 서로 다른 두 시스템에 데이터를 쓸 때 발생하는 이중 쓰기 문제를 해결하는 패턴입니다.
그렇다면 Transaction Outbox Pattern을 사용하지 않는다면 무슨 문제가 발생할까요?
우선 기존 아키텍처는 하나의 Lambda 함수에서 각 DB에 데이터를 저장합니다.
DB가 다르기 때문에 각 DB에 저장하는 트랜잭션은 서로 독립적입니다.
여기서 하나의 트랜잭션이 실패하게 되면 해당 트랜잭션은 롤백되지만 다른 트랜잭션은 롤백되지 않고 그대로 커밋되게 됩니다.


즉, 정합성이 깨지게 되는 것입니다.
이처럼 서로 다른 두 서비스에 대한 데이터 정합성을 확보하기 위해 관계형 데이터베이스에 outbox 테이블을 사용해서 Outbox Transaction Pattern을 구현할 수 있습니다.
outbox table에 삽입되는 데이터는 식당 메타 데이터를 저장했다는 의미를 가지고 있고 해당 식당에 대한 Vector 데이터의 저장 여부를 컬럼으로 가지고 있어야 합니다.
그래서 식당 DB에 outbox table을 생성한 다음 메타 데이터를 저장하는 연산과 outbox table에 데이터를 삽입하는 연산을 하나의 트랜잭션으로 수행합니다.
그리고 outbox table은 주기적으로 확인해서 아직 Vector 데이터가 저장되지 않은 식당이 있다면 vector 데이터를 저장한 뒤 저장 여부를 true로 바꾸어줍니다.

아키텍처의 흐름은 다음과 같습니다.

이제 저장에 실패하는 경우를 생각해봅시다.
처음에 식당 DB 메타데이터 저장이 제약 조건 같은 사항을 위배해서 실패했다고 합시다.
메타 데이터 저장과 outbox table 데이터 삽입이 같은 트랜잭션에서 수행되기 때문에 롤백되면서 outbox table에 데이터가 삽입되지 않습니다.
outbox table에 데이터가 삽입되지 않기 때문에 vector 데이터 저장 연산 또한 수행되지 않습니다.
이렇게 Transaction Outbox Pattern을 적용하여 서로 다른 두 DB에 대한 연산의 정합성을 확보할 수 있게 되었습니다.
구현 과정
우선 outbox table은 다음과 같습니다.

restaurant_id는 메타 데이터가 저장된 식당의 PK 값이고 payload는 메시지 큐에 전달할 메시지, is_processed는 Vector 데이터 저장 여부입니다.
저는 payload에 S3 key 값을 넣었고 polling을 통해 처리되지 않은 식당의 vector 데이터가 담긴 S3 파일의 키 값을 message queue에 계속 넣어주었습니다.
그렇게 payload를 message queue에 넣은 다음 is_processed 값을 true로 변경하였습니다.
최종 아키텍처는 다음과 같습니다.

정리
이렇게 이번 포스팅에서는 Transaction Outbox Pattern을 적용하여 데이터 정합성 문제를 해결해보았습니다.
물론 Transaction Outbox Pattern이 완벽하게 데이터 정합성을 지켜준다고 볼 수는 없습니다.
기본적으로 계속 outbox table을 polling하는 구조이기 때문에 polling하기 전까지는 Vector DB에 데이터가 저장되지 않습니다.
즉 실시간성이 지켜지지 않는 것입니다.
또한 Vector DB 데이터 저장에 실패한다고 해서 메타 데이터 저장 연산이 롤백되지 않습니다.
하지만 Transaction Outbox Pattern을 구현하면서 Vector DB에는 데이터가 저장되었지만 식당 DB에는 데이터가 저장되지 않는 상황은 방지할 수 있습니다.
기존에 문제가 되었던 유사도 검색으로 조회한 식당 id의 메타 데이터를 조회하지 못 해 에러가 발생하는 문제를 해결할 수 있었습니다.
출처
트랜잭션 아웃박스 패턴 - AWS 권장 가이드
이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.
docs.aws.amazon.com