본문 바로가기
Backend/Database

[Trino] 여러 MySQL 데이터 소스를 Trino로 통합 조회하기

미지시료 2026. 8. 29.
반응형

여러 MySQL 서버에 분산된 데이터를 Catalog로 추상화하고, 기존 MySQL 조회문을 별도 변환 서비스와 라우팅 로직을 거쳐 Trino SQL로 바꾼 과정을 정리했다.





시작하며

서비스가 처음 만들어졌을 때는 하나의 MySQL 연결만으로 필요한 데이터를 조회할 수 있었다. 이후 데이터 규모와 운영 단위가 늘어나면서 데이터가 여러 MySQL 서버로 나뉘었다.

여러 운영 단위의 데이터
  → 서로 다른 MySQL 서버에 분산 저장
  → Trino를 통해 통합 조회

애플리케이션이 각 MySQL에 직접 연결하도록 만들면 조회할 때마다 다음 작업이 필요하다.

  1. 요청 대상의 데이터 위치를 확인한다.
  2. 해당 서버의 연결 풀을 선택한다.
  3. 쿼리를 실행한다.
  4. 여러 서버에 걸친 결과를 애플리케이션에서 합친다.
  5. 데이터 소스가 추가되면 연결 설정과 분기 로직을 수정한다.

특히 서로 다른 서버의 데이터를 하나의 화면에서 비교하거나 합쳐야 할 때 문제가 커진다. 애플리케이션이 사실상 작은 분산 쿼리 엔진 역할까지 맡게 되기 때문이다.

이 문제를 줄이기 위해 여러 MySQL을 Trino Catalog로 등록하고, 조회 애플리케이션은 Trino 하나만 호출하도록 구성했다.





Trino를 선택한 이유

Trino는 여러 데이터 소스를 SQL로 조회할 수 있는 분산 SQL Query Engine이다. MySQL 서버마다 별도의 Catalog를 등록하면 하나의 SQL에서 서로 다른 서버의 테이블을 조회할 수 있다.

SELECT id, created_at
FROM catalog_a.analytics.records

UNION ALL

SELECT id, created_at
FROM catalog_b.analytics.records;

여기서 중요한 것은 데이터를 Trino 내부로 미리 복사하지 않는다는 점이다. Trino는 쿼리를 실행할 때 각 Connector를 통해 원본 데이터 소스를 읽고 결과를 처리한다.

이 구조를 선택한 목적은 다음과 같았다.

  • 애플리케이션의 DB 연결 분기를 줄인다.
  • 여러 MySQL에 걸친 JOIN과 UNION을 SQL로 표현한다.
  • 데이터 소스가 추가돼도 조회 API의 연결 지점은 유지한다.
  • 데이터 위치와 실제 연결 정보를 Catalog 설정으로 분리한다.

Trino가 데이터 배치나 저장소를 대체하는 것은 아니다. 이 프로젝트에서는 분산된 운영 데이터를 하나의 조회 계층으로 묶는 역할에 집중했다.





전체 조회 구조

대시보드 위젯과 조회 설정에 저장된 MySQL SQL은 바로 Trino로 전달되지 않는다. 별도 서비스에서 SQL 문법을 변환하고, 조회 애플리케이션이 실제 데이터 위치를 찾아 테이블명을 완전 수식 이름으로 바꾼 뒤 실행한다.

여러 MySQL을 Trino Catalog로 통합한 조회 구조

전체 흐름은 다음과 같다.

  1. Query API가 위젯이나 조회 설정에 저장된 MySQL SQL을 읽는다.
  2. Query API가 HTTP로 SQL Convert API에 변환을 요청한다.
  3. Flask와 sqlglot으로 구성한 변환 서비스가 MySQL 문법을 Trino Dialect로 변환한다.
  4. Query API 내부의 라우팅 로직이 조회 조건에서 식별자를 찾는다.
  5. 메타데이터를 이용해 식별자가 저장된 데이터 소스를 결정한다.
  6. 논리 테이블을 catalog.schema.table로 치환한다.
  7. Trino Coordinator에 최종 SQL을 전달한다.
  8. Coordinator가 실행 계획을 만들고 Worker가 각 MySQL에서 데이터를 읽는다.
  9. Trino가 처리한 결과를 Query API가 반환한다.

대시보드와 조회 기능을 사용하는 쪽에서는 실제 Catalog 이름이나 MySQL 주소를 알 필요가 없다.





처음부터 현재 구조였던 것은 아니다

처음 구현할 때는 데이터 소스 주소와 Catalog 정보를 TypeScript 객체에 직접 적었다. 조회 기능을 빠르게 연결하는 데는 편했지만, 배포 환경이 늘어나자 같은 코드가 환경마다 다른 데이터 소스를 알아야 하는 문제가 생겼다.

실제 구성은 다음 순서로 바뀌었다.

단계 처음 적용한 방식 실제로 발견한 문제 변경한 내용
1 코드에 Catalog 매핑을 고정 환경마다 대상 MySQL과 Catalog 구성이 다름 환경별 JSON 설정으로 분리
2 __dirname 기준으로 JSON 로드 개발 실행에서는 읽히지만 빌드 결과에서는 파일 위치가 달라짐 빌드 Asset에 포함하고 실행 위치별 후보 경로 확인
3 애플리케이션에서 Catalog 이름 지정 Trino에 등록한 이름과 한 글자라도 다르면 조회 실패 Connector 파일명과 애플리케이션 매핑을 같은 이름으로 수정
4 매핑에 저장 구조 버전만 기록 데이터 서비스의 실제 버전과 어긋나도 연결 자체는 성공함 실행 중인 데이터 서비스와 버전을 대조하는 점검 기능 추가

가장 찾기 어려웠던 문제는 네 번째였다. Catalog 이름이 틀리면 Trino가 비교적 명확한 오류를 반환한다. 반면 저장 구조 버전이 틀리면 존재하지 않는 형식의 테이블을 찾거나 다른 테이블을 조회해 서비스는 정상인데 결과만 비어 보일 수 있었다.

이 경험 이후에는 설정 파일이 존재하는지만 확인하지 않고 다음 질문을 분리해서 확인했다.

  1. 애플리케이션이 의도한 환경의 매핑 파일을 읽었는가
  2. 매핑의 Catalog 이름이 Trino의 실제 Catalog 이름과 같은가
  3. Catalog가 가리키는 데이터 소스에 접속할 수 있는가
  4. 매핑의 테이블 형식이 데이터 소스의 실제 저장 구조와 같은가





MySQL 하나를 Catalog 하나로 등록하기

Trino에서 Catalog는 Connector 설정에 이름을 붙인 단위다. 다음처럼 MySQL Connector 설정 파일을 만들면 파일명이 SQL에서 사용하는 Catalog 이름이 된다.

# catalog_a.properties
connector.name=mysql
connection-url=jdbc:mysql://MYSQL_A_HOST:3306
connection-user=READ_ONLY_USER
connection-password=PASSWORD

SQL에서는 다음과 같이 접근한다.

catalog.schema.table
SELECT *
FROM catalog_a.analytics.records;

Catalog는 단순한 별칭 이상이다. 어떤 Connector를 사용하고, 어느 서버에 어떤 계정으로 연결할지를 정의한다. 따라서 애플리케이션 코드에 MySQL 연결 정보를 직접 늘리지 않고도 Trino 설정을 통해 데이터 소스를 추가할 수 있다.

MySQL 계정은 읽기 전용으로 제한하는 것이 안전하다. 조회 전용 계층에서 쓰기 권한까지 보유할 이유가 없고, SQL 검증의 실수가 실제 데이터 변경으로 이어지는 것을 막을 수 있기 때문이다.





논리 SQL과 물리 SQL을 분리하기

조회 설정에서 Catalog를 직접 지정하게 하면 내부 인프라 구성이 조회 계약으로 노출된다. 데이터가 다른 서버로 이동할 때 저장된 SQL도 함께 바뀌는 문제가 생긴다.

그래서 위젯과 조회 설정에는 논리 테이블만 사용하도록 했다.

SELECT recorded_at, value
FROM records
WHERE scope_id = 42;

조회 애플리케이션은 scope_id를 기준으로 실제 데이터 위치를 찾고 다음과 같이 변환한다.

SELECT recorded_at, value
FROM catalog_b.analytics.records
WHERE scope_id = 42;

이 구조에서 메타데이터는 다음 관계를 가진다.

scope_id
  → data_source_key
  → catalog + schema + table format

데이터가 이동해도 매핑만 변경하면 논리 SQL과 외부 API를 유지할 수 있다.





SQL Dialect 변환을 먼저 수행하기

기존 조회 설정의 SQL은 MySQL 문법을 사용하고 있었다. MySQL과 Trino는 날짜 함수, 문자열 함수, identifier quoting과 일부 표현식 문법이 다르다.

간단한 문자열 치환으로 SQL 전체를 바꾸면 문자열 리터럴이나 중첩 표현식까지 잘못 수정할 수 있다. 따라서 Flask로 만든 별도 SQL Convert API에서 sqlglot이 MySQL AST를 읽고 Trino SQL로 다시 생성하도록 했다.

Query API (NestJS)
  → HTTP
SQL Convert API (Flask + sqlglot)
  → 변환된 Trino SQL
Query API의 Catalog·Table 라우팅

SQL Convert API 자체는 MySQL에 접속하지 않는다. SQL 문자열을 받아 변환 결과만 반환하며, 데이터 위치 확인과 Catalog 치환, 실제 쿼리 실행은 Query API와 Trino가 담당한다. 따라서 SQL 변환 기능만 실행할 때는 데이터 동기화 기능에서 사용하는 DB 접속 설정도 필요하지 않다.

변환 서비스는 SQL을 AST로 파싱한 뒤, MySQL과 Trino에서 의미가 달라지는 표현식을 보정하고 Trino SQL로 다시 생성한다.

tree = sqlglot.parse_one(sql, read="mysql")
tree = tree.transform(_localizer(tz))

converted = tree.sql(
    dialect="trino",
    pretty=True,
    unsupported_level=sqlglot.ErrorLevel.RAISE,
)

return _MONTH_OF_CURRENT_DATE.sub(
    "date_trunc('month', CURRENT_DATE)",
    converted,
)
MySQL SQL → AST 파싱 → 의미 보정 → Trino SQL 생성

SQL 문법이 변환됐다고 해서 결과의 의미까지 항상 같아지는 것은 아니었다. 시간대가 적용되는 날짜·시간 표현식과 주차 계산, 사용자 지정 정렬처럼 실제 결과가 달라질 수 있는 표현식은 AST를 직접 변환했다. AST 단계에서 처리하기 어려운 확인된 패턴만 마지막에 제한적으로 보정했다.

sqlglot AST 파싱
  → 시간대 · 주차 · 정렬 의미 보정
  → Trino Dialect SQL 생성
  → 확인된 일부 패턴 후처리
  → Trino SQL 반환

즉 SQL 전체를 정규식으로 번역한 것이 아니다. 구문 분석과 일반적인 Dialect 변환은 sqlglot에 맡기고, 실제 실행으로 확인한 의미 차이만 AST와 제한적인 후처리로 보정했다.

변환 결과를 받은 뒤에 Catalog와 물리 테이블을 치환한다. 처리 순서를 반대로 하면 변환 모듈이 애플리케이션에서 생성한 완전 수식 테이블이나 서브쿼리를 다시 해석해야 하므로 책임이 복잡해진다.

오류도 원인에 따라 구분했다.

상황 Query API 응답 의미
입력 SQL 파싱 실패 400 잘못된 요청
지원하지 않는 SQL 표현 400 잘못된 요청
변환 서비스에 연결할 수 없음 503 일시적인 서비스 장애
그 밖의 변환 서비스 오류 500 내부 서버 오류

조회 SQL 문제와 내부 의존 서비스 장애를 같은 오류로 반환하면 재시도 여부를 판단하기 어렵다. 변환 서비스에는 별도의 상태 확인 Endpoint도 두어 프로세스가 응답하는지 확인할 수 있게 했다.





테이블을 실제 Catalog로 라우팅하기

SQL Dialect 변환이 끝나면 FROMJOIN 뒤의 테이블을 찾고, 각 테이블이 어느 Catalog에 있는지 결정한다.

논리 SQL을 실제 Trino SQL로 바꾸는 과정

일반적인 라우팅 과정은 다음과 같다.

논리 테이블 records
  + 조건 scope_id = 42
  → scope 42의 data_source_key 조회
  → catalog 매핑 확인
  → catalog_b.analytics.records

테이블이 하나이고 식별자가 하나라면 비교적 간단하다. 문제는 JOIN과 UNION이 포함됐을 때다.





JOIN에서는 alias별 식별자를 확인하기

하나의 SELECT에 여러 테이블이 등장하면 단순히 scope_id = 42만 찾아서는 어느 테이블의 조건인지 알 수 없다.

SELECT a.value, b.status
FROM records a
JOIN status_log b ON a.record_id = b.record_id
WHERE a.scope_id = 42
  AND b.scope_id = 84;

이 경우 a.scope_idrecords a의 Catalog를, b.scope_idstatus_log b의 Catalog를 결정하는 데 우선 사용한다.

현재 라우팅 규칙은 다음과 같다.

  1. 테이블 alias가 있으면 alias.scope_id = 값을 우선 사용한다.
  2. alias 조건을 찾지 못하면 SELECT 블록 전체의 scope_id를 수집한다.
  3. 고유한 값이 하나뿐이면 해당 블록의 공통 식별자로 사용한다.
  4. 식별자가 없거나 서로 다른 값이 여러 개라면 데이터 위치를 결정하지 않는다.

이 fallback은 JOIN된 테이블이 같은 데이터 소스에 있다는 전제에서는 유용하다. 반면 테이블마다 다른 데이터 소스를 가리키는데 alias 조건이 충분하지 않다면 각 조건의 소유 관계를 정확히 판단할 수 없다. 이런 쿼리까지 안전하게 처리하려면 문자열 탐색이 아니라 SQL AST를 기준으로 테이블과 조건의 관계를 분석해야 한다.





UNION의 SELECT 블록은 독립적으로 처리하기

UNION ALL로 연결된 각 SELECT가 서로 다른 데이터 소스를 조회할 수도 있다.

SELECT recorded_at, value
FROM records
WHERE scope_id = 42

UNION ALL

SELECT recorded_at, value
FROM records
WHERE scope_id = 84;

SQL 전체에서 식별자 하나만 찾으면 두 번째 SELECT까지 같은 Catalog로 치환된다. 그래서 UNION ALL을 기준으로 SELECT 블록을 나누고 각 블록을 독립적으로 라우팅한 뒤 다시 결합했다.

SELECT recorded_at, value
FROM catalog_a.analytics.records
WHERE scope_id = 42

UNION ALL

SELECT recorded_at, value
FROM catalog_c.analytics.records
WHERE scope_id = 84;

이 방식은 단순한 UNION에는 효과적이지만 문자열과 주석, 중첩 서브쿼리에 포함된 UNION ALL까지 정확히 구분하려면 정규식보다 SQL AST가 필요하다.





물리 테이블 구조 차이를 매핑에 포함하기

모든 MySQL이 같은 시기에 구축된 것은 아니므로 테이블 구성 규칙도 다를 수 있다. 이를 코드의 조건문으로 흩뜨리지 않고 Catalog 매핑에 포함했다.

{
  "node-a": {
    "catalog": "catalog_a",
    "schema": "analytics",
    "tableFormat": "records_{scopeId}_{partitionKey}"
  },
  "node-b": {
    "catalog": "catalog_b",
    "schema": "analytics",
    "tableFormat": "records_{scopeId}"
  }
}

애플리케이션은 데이터 소스에 맞는 형식으로 물리 테이블명을 생성한다. 조회 범위가 여러 파티션에 걸치면 필요한 테이블을 UNION ALL 서브쿼리로 묶는다.

SELECT *
FROM (
  SELECT * FROM catalog_a.analytics.records_42_p1
  UNION ALL
  SELECT * FROM catalog_a.analytics.records_42_p2
) AS records;

필요한 테이블이 하나라면 완전 수식 이름으로만 치환하고, 여러 개일 때만 서브쿼리를 생성한다. 외부 SQL은 논리 테이블 records를 계속 사용한다.

3번 글이 서비스 내부에서 스키마별 Repository를 선택하는 방법을 다뤘다면, 여기서는 여러 원격 데이터 소스의 물리 위치를 Trino SQL로 라우팅하는 문제에 초점을 둔다.





Catalog 설정은 두 위치에서 일치해야 한다

Catalog를 추가할 때는 Trino 설정과 애플리케이션 매핑을 함께 변경해야 한다.

Trino 설정
catalog_a.properties
        ↕ 이름이 정확히 일치해야 함
애플리케이션 매핑
"catalog": "catalog_a"

한쪽만 변경하면 다음과 같은 문제가 발생한다.

  • Trino에만 등록하면 애플리케이션이 해당 Catalog를 선택하지 못한다.
  • 애플리케이션에만 등록하면 Trino가 존재하지 않는 Catalog라고 응답한다.
  • 일부 Worker에만 설정하면 작업이 그 Worker에 배정될 때만 간헐적으로 실패한다.

Trino Worker는 특정 Catalog에 전용으로 묶이지 않는다. Coordinator가 실행 계획에 따라 작업을 배정하므로 Coordinator와 모든 Worker가 동일한 Catalog 목록을 가져야 한다.

환경별 매핑 파일은 애플리케이션 시작 시 로드하고 비어 있거나 형식이 잘못됐다면 기동을 실패시키도록 했다. 핵심 라우팅 설정 오류를 첫 요청까지 숨기지 않기 위해서다.





저장 구조 버전이 다르면 정상 연결도 잘못된 결과를 낸다

Catalog 매핑에는 연결 위치뿐 아니라 물리 테이블을 만드는 규칙도 들어 있다. 같은 데이터 소스라도 저장 구조 버전에 따라 다음처럼 테이블 형식이 달라질 수 있기 때문이다.

version 1 → records_{scopeId}_{groupId}_{partition}
version 2 → records_{scopeId}_{partition}

문제는 이 버전이 두 곳에 존재한다는 점이었다.

조회 애플리케이션의 Catalog 매핑 version
데이터 서비스가 실제로 사용하는 schema version

둘이 다르면 조회 애플리케이션은 첫 번째 형식으로 SQL을 만들고, 데이터 서비스는 두 번째 형식의 테이블에 데이터를 저장할 수 있다. 양쪽 프로세스와 MySQL 연결은 모두 정상이라 단순 상태 확인만으로는 원인을 찾기 어렵다.

그래서 데이터 서비스의 상태 확인 응답에 실제 버전을 포함하고, 조회 애플리케이션이 Catalog 매핑 값과 대조하도록 했다.

상태 의미 처리 방식
match Catalog 매핑과 데이터 서비스 버전이 같음 정상으로 기록
mismatch 두 버전이 서로 다름 어떤 데이터 소스가 어긋났는지 경고
unreachable 데이터 서비스가 응답하지 않거나 버전을 제공하지 않음 연결 문제와 오래된 서비스 가능성을 구분해 기록

기동 시 대조는 수행하지만 한 데이터 소스가 응답하지 않는다는 이유로 조회 애플리케이션 전체를 종료하지는 않았다. 일부 데이터 소스의 장애가 나머지 조회 기능까지 막지 않도록 하기 위해서다.

반대로 Catalog 매핑 파일 자체가 없거나 비어 있는 경우에는 기동을 중단한다. 모든 조회에 필요한 로컬 설정 오류와 일부 원격 데이터 소스의 장애는 실패 범위가 다르기 때문이다.

필수 로컬 매핑 오류   → Fail Fast
개별 데이터 소스 장애 → 경고 후 나머지 기능 유지

이 대조 기능은 어느 쪽 설정이 옳은지 자동으로 결정하지 않는다. 서로 다르다는 사실만 알려주며, 운영자는 실제 배포된 스키마를 확인한 뒤 한쪽을 수정해야 한다. 같은 정보를 두 곳에 적는 구조를 없애는 것이 근본적인 해결이고, 대조는 그전까지 오류를 조기에 발견하기 위한 안전장치다.





특정 Catalog 장애와 전체 서비스 장애를 구분하기

Trino Coordinator가 실행 중이어도 특정 MySQL에 연결하지 못할 수 있다.

Trino 정상
├── Catalog A 정상
├── Catalog B 연결 실패
└── Catalog C 정상

따라서 상태 확인도 층을 나눠야 한다.

  • Coordinator 프로세스가 응답하는가
  • 필요한 Catalog가 등록되어 있는가
  • Catalog를 통해 대상 Schema를 조회할 수 있는가
  • 실제 테이블에 짧은 쿼리를 실행할 수 있는가

선택 기능에서만 사용하는 메타데이터 로딩 실패는 해당 기능만 비활성화하고 애플리케이션 기동을 계속할 수 있다. 반면 기본 Catalog 매핑처럼 모든 조회에 필요한 설정이 잘못됐다면 시작 단계에서 실패시키는 편이 안전하다.





데이터 소스를 추가할 때 확인할 체크리스트

새 MySQL을 Catalog로 추가하는 작업은 properties 파일 하나를 만드는 것으로 끝나지 않았다. 다음 항목을 함께 확인해야 같은 문제가 반복되지 않는다.

  • MySQL 조회 계정에 필요한 테이블의 읽기 권한만 부여했는가
  • Coordinator와 모든 Worker에 같은 이름의 Catalog 설정이 배포됐는가
  • 애플리케이션 매핑의 Catalog 이름이 Connector 설정 파일명과 일치하는가
  • 환경에 맞는 매핑 JSON이 빌드 결과물에 포함됐는가
  • Schema와 물리 테이블 형식이 실제 데이터 소스와 일치하는가
  • Catalog 매핑 버전과 데이터 서비스의 실제 버전이 일치하는가
  • Trino에서 Catalog와 Schema 목록을 조회할 수 있는가
  • 대표 테이블에 작은 범위의 SELECT를 실행할 수 있는가
  • 단일 데이터 소스뿐 아니라 JOIN과 UNION 라우팅도 확인했는가
  • 실패했을 때 Catalog, 데이터 소스, 최종 SQL을 추적할 로그가 남는가

확인 순서도 중요하다. 애플리케이션의 복잡한 조회부터 실행하면 SQL 변환, 라우팅, Catalog와 원본 MySQL 중 어디에서 실패했는지 구분하기 어렵다.

SHOW CATALOGS;
-- 통과하면
SHOW SCHEMAS FROM catalog_a;
-- 통과하면
SELECT * FROM catalog_a.analytics.records LIMIT 1;

이 세 단계가 통과한 뒤 애플리케이션의 단일 테이블 조회와 JOIN·UNION 통합 조회를 실행한다. 가장 작은 경계부터 하나씩 통과시키면 연결 문제와 SQL 재작성 문제를 분리해서 확인할 수 있다.





조회 SQL을 실행할 때 필요한 안전장치

현재 SQL Convert API가 보장하는 것은 MySQL 문법을 파싱하고 Trino로 표현할 수 없는 구문을 실패시키는 데까지다. 파싱에 성공했다는 사실이 해당 SQL을 실행해도 안전하다는 의미는 아니다.

현재 흐름은 저장된 조회 설정을 실행하지만, 조회 SQL을 수정할 수 있는 범위가 넓어지거나 외부에서 임의 SQL을 받게 된다면 별도의 실행 제한이 필요하다. Trino와 MySQL 계정을 읽기 전용으로 설정하는 것만으로 충분하지 않다.

운영 환경에서는 다음 제한이 필요하다.

  • SELECT와 허용된 CTE만 실행한다.
  • 접근 가능한 논리 테이블을 allowlist로 관리한다.
  • 클라이언트가 Catalog와 Schema를 직접 지정하지 못하게 한다.
  • 라우팅 식별자에 대한 사용자 권한을 먼저 확인한다.
  • 실행 시간, 스캔 데이터, 결과 행 수에 제한을 둔다.
  • 실행한 SQL과 사용자, 대상 데이터 소스를 감사 로그로 남긴다.

정규식으로 첫 단어가 SELECT인지 확인하는 정도로는 주석, 다중 Statement와 우회 문법을 안전하게 막기 어렵다. SQL Parser의 AST를 기준으로 허용 노드와 테이블을 검증하는 것이 바람직하다.





적용하면서 확인한 한계

정규식 기반 SQL 재작성

단순한 FROM, JOIN, UNION ALL은 처리할 수 있지만 CTE, 중첩 서브쿼리, 주석과 복잡한 alias가 추가되면 경계가 무너질 수 있다. SQL 변환뿐 아니라 테이블 라우팅도 AST 기반으로 옮기는 것이 장기적으로 안전하다.

Catalog 설정의 이중 관리

Trino의 properties 파일과 애플리케이션의 Catalog 매핑이 따로 존재한다. 데이터 소스를 추가할 때 두 위치와 모든 Trino 노드를 함께 변경해야 한다. 설정 생성과 배포, 재시작, 검증을 자동화할 필요가 있다.

네트워크에 의존하는 조회 성능

Trino는 원본 MySQL을 원격으로 읽는다. 여러 Catalog를 조회하면 가장 느린 네트워크와 데이터 소스가 전체 쿼리 시간을 결정할 수 있다. Predicate Pushdown 여부와 원본 인덱스, 조회 범위를 함께 확인해야 한다.

SQL 변환 서비스라는 추가 의존성

SQL Dialect 변환을 별도 서비스가 담당하면 Query API와 Trino가 정상이어도 변환 서비스 장애로 모든 조회가 실패할 수 있다. 변환 서비스에는 상태 확인 Endpoint를 두고 Query API에서는 잘못된 SQL, 무응답과 내부 오류를 서로 다른 상태로 변환했다.

다만 변환 요청의 제한 시간, Circuit Breaker와 변환 처리 시간 지표는 추가로 보완할 영역이다. 오류를 구분하는 것과 장애가 전체 조회 경로로 전파되는 것을 막는 것은 별개의 문제다.

완전한 위치 투명성의 어려움

Catalog를 숨겨도 데이터 소스마다 지원하는 함수, 컬럼 타입과 성능 특성이 다를 수 있다. 논리 SQL 계약에서 허용할 기능을 제한하고 데이터 소스 간 호환성을 지속적으로 검증해야 한다.





마치며

여러 MySQL을 Trino로 연결하는 것만으로 애플리케이션의 문제가 모두 해결되지는 않았다. 실제로 필요한 것은 논리 SQL과 물리 데이터 위치 사이의 라우팅 계층이었다.

정리하면 다음과 같다.

  1. MySQL 데이터 소스를 Catalog로 추상화한다.
  2. 위젯과 조회 설정에는 논리 테이블만 사용한다.
  3. 별도 서비스에서 sqlglot으로 MySQL SQL을 Trino Dialect로 변환한다.
  4. 조회 식별자를 이용해 실제 Catalog와 Schema를 결정한다.
  5. UNION의 각 SELECT를 나누고 JOIN에서는 alias 조건을 우선해 라우팅한다.
  6. 물리 테이블 규칙은 환경별 매핑으로 관리한다.
  7. Trino 설정과 애플리케이션 매핑을 함께 배포하고 검증한다.
  8. Catalog 매핑의 저장 구조 버전과 데이터 서비스의 실제 버전을 대조한다.
  9. 필수 로컬 설정 오류와 개별 원격 데이터 소스 장애의 실패 범위를 구분한다.
  10. 임의 SQL 입력까지 허용하려면 Statement 종류와 접근 테이블, 실행 범위를 별도로 제한해야 한다.

Trino의 가장 큰 장점은 여러 데이터 소스에 SQL 하나로 접근할 수 있다는 점이다. 하지만 그 장점을 안전하게 사용하려면 데이터 위치를 결정하는 규칙, SQL 재작성, 설정 동기화와 운영 관측성까지 함께 설계해야 한다.

반응형