본문으로 건너뛰기
  1. 글 목록/

Superset 사용자 제보 이슈 6건 대응기

·7 분
작성자
nanta
Apache Kafka, Airflow, Trino, StarRocks 등 데이터 엔지니어링과 모던 데이터 인프라에 대한 실무 경험을 공유하는 블로그입니다.

데이터 거버넌스팀에서 Superset을 적극적으로 활용하시면서 발견한 이슈 6건을 공유해주셨다. 직접 사용하면서 제보해주신 덕분에 우리가 내부 테스트에서는 놓쳤을 문제들을 잡을 수 있었다.

이슈의 난이도는 다양했다. 코드 한 줄 수정이면 해결되는 것도 있었고, Superset 서버 로그와 RDS 에러 로그를 교차로 추적해서 겨우 원인을 찾은 것도 있었다. 이슈별로 원인과 대응을 정리한다.


1. Slack 차트 스크린샷 확장자 누락
#

제보 내용: Superset에서 Slack으로 피딩되는 차트 스크린샷 파일을 다운로드하면 확장자가 없다. 파일을 열려고 하면 OS가 이미지 포맷을 인식하지 못해서 별도로 확장자를 붙여줘야 한다.

원인: Superset이 Slack API의 files.upload를 호출할 때 filename 파라미터에 확장자를 포함하지 않고 있었다. Slack은 업로드 시 전달받은 파일명을 그대로 사용한다. 파일의 실제 내용은 PNG 이미지인데, 파일명이 chart_screenshot처럼 확장자 없이 전달되면 Slack에서도 확장자 없는 파일명으로 저장된다.

macOS에서는 파일 내용을 기반으로 타입을 추론하는 경우가 많아서 확장자가 없어도 이미지로 열리는 경우가 있다. 하지만 Windows에서는 확장자에 의존하기 때문에 이미지로 인식하지 못한다. 제보해주신 분이 Windows를 사용하고 계셔서 이 문제를 발견하신 것이다.

대응: Slack API 호출 시 파일명에 .png 확장자를 포함하도록 코드를 수정했다.


2. 대시보드 자동 새로고침 초기화
#

제보 내용: 대시보드 화면에서 auto-refresh interval을 설정했는데, 브라우저를 닫고 다시 접속하면 설정이 초기화돼 있다.

원인: 대시보드 우측 상단의 auto-refresh 메뉴는 현재 브라우저 세션에만 적용되는 설정이다. Superset의 설계상 이 값은 서버에 저장되지 않는다. 브라우저 탭을 닫으면 사라진다.

혼란의 원인은 Superset에 같은 목적의 설정이 두 군데 있기 때문이다.

  • 세션 레벨 설정: 대시보드 우측 상단의 auto-refresh 드롭다운. 현재 세션에만 적용되고, 브라우저를 닫으면 초기화된다.
  • 대시보드 레벨 설정: 대시보드 편집 모드에서 설정하는 리프레시 인터벌. 대시보드 메타데이터에 저장되므로, 누가 접속하든 동일하게 적용된다.

제보자분은 세션 레벨 설정을 사용하고 계셨다. 대시보드 레벨 설정의 존재 자체를 모르셨는데, 이건 UX 설계의 문제라고 생각한다. 같은 목적의 설정이 스코프만 다르게 두 곳에 있으면 사용자 입장에서는 혼란스러울 수밖에 없다.

대응: 대시보드 편집 모드에서 리프레시 인터벌을 설정하는 방법을 안내했다.


3. SqlLab 데이터셋 덮어쓰기 버그
#

제보 내용: SqlLab에서 쿼리 결과를 데이터셋으로 저장할 때 “overwrite existing dataset” 옵션을 선택하면, 기존 데이터셋 목록이 제대로 표시되지 않는다. 목록이 빈 상태거나, 검색이 동작하지 않는다.

원인: Superset 업스트림의 알려진 버그였다. 데이터셋 목록을 불러오는 API 엔드포인트에서 필터링 로직이 잘못 동작하고 있었다.

이 이슈는 제보를 받고 Superset의 GitHub Issues를 먼저 검색했다. 동일한 이슈가 이미 보고되어 있었고, 픽스 PR도 머지된 상태였다. 다만 우리가 운영하는 Superset 버전에는 아직 반영되지 않은 커밋이었다.

대응: 해당 픽스 커밋을 우리 포크 레포에 체리피킹해서 적용했다.


4. 윈도우 환경 CSV 한글 깨짐
#

제보 내용: Superset에서 다운로드한 CSV 파일을 윈도우 환경의 Excel에서 열면 한글이 전부 깨져서 보인다. 메모장에서 열면 정상인데, Excel에서만 깨진다.

원인: Superset이 CSV를 표준 UTF-8로 인코딩해서 내보내고 있었다. 정상적인 UTF-8 파일이고, 메모장이나 대부분의 텍스트 에디터에서는 문제없이 열린다.

문제는 Microsoft Excel이다. Excel은 CSV 파일을 열 때 파일 앞부분에 BOM(Byte Order Mark)이 있는지를 확인한다. BOM은 파일의 처음 3바이트(EF BB BF)에 위치하는 특수 마커로, “이 파일은 UTF-8입니다"라고 알려주는 역할을 한다. BOM이 없으면 Excel은 시스템 기본 인코딩으로 해석하는데, 한국어 Windows의 기본 인코딩은 EUC-KR(CP949)이다.

UTF-8로 인코딩된 한글을 EUC-KR로 해석하면 깨진다. 예를 들어 “서울"이라는 문자열은 UTF-8에서 EC 84 9C EC 9A B8인데, 이걸 EUC-KR로 해석하면 전혀 다른 문자가 된다.

macOS나 Linux에서는 시스템 기본 인코딩이 UTF-8이기 때문에 BOM이 없어도 문제가 되지 않는다. 하지만 윈도우 Excel 사용자가 많은 환경에서는 BOM을 반드시 포함해야 한다.

대응: CSV 내보내기 인코딩을 utf-8에서 utf-8-sig로 변경했다. utf-8-sig는 Python 표준 라이브러리에서 제공하는 인코딩 변형으로, 파일 시작 부분에 BOM 3바이트(EF BB BF)를 자동으로 삽입한다. 코드 변경은 인코딩 파라미터 하나를 바꾸는 것뿐이다.

# Before
df.to_csv(path, encoding='utf-8')

# After
df.to_csv(path, encoding='utf-8-sig')

BOM이 포함되면 Excel이 파일을 UTF-8로 올바르게 인식해서 한글이 정상 표시된다.

참고로 BOM은 UTF-8 표준에서 선택사항이고, BOM이 있는 UTF-8 파일을 일부 유닉스 도구에서 비정상으로 취급하는 경우가 있다. 하지만 CSV의 주 소비처가 Excel인 환경에서는 BOM을 넣는 게 맞다.


5. 쿼리 결과 행 제한 10만 → 100만
#

제보 내용: Superset에서 쿼리 결과가 10만 행으로 제한되어 있다. 분석 업무상 100만 행까지 출력되면 좋겠다.

대응: Superset의 ROW_LIMIT 관련 설정값을 변경해서 최대 100만 행까지 응답하도록 조정했다.

단순한 설정 변경이지만, 행 제한을 무조건 올리는 것이 항상 좋은 건 아니다. 행 수가 늘어나면 Superset 웹서버의 메모리 사용량과 응답 시간이 비례해서 증가한다. 100만 행이면 컬럼 수와 데이터 타입에 따라 수 GB의 메모리를 점유할 수 있다.

우리 환경에서는 Superset 웹서버의 메모리 리소스에 여유가 있어서 100만 행으로 올렸지만, 이건 운영 환경의 리소스 상황을 보면서 결정해야 하는 설정이다. 만약 메모리 OOM이 발생하면 다시 낮출 수도 있다.

그리고 행 제한을 올리면 다음 이슈(6번)처럼 대용량 CSV 다운로드에서 문제가 생길 여지도 커진다. 관련이 깊은 이슈라서 함께 대응했다.


6. CSV 다운로드 500 에러 — 가장 까다로웠던 이슈
#

제보 내용: Superset에서 50만 행 이상의 쿼리 결과를 CSV로 다운로드하면 약 1분 뒤에 500 에러가 발생한다. 20만 행까지는 문제없이 다운로드된다.

이 이슈는 제보 내용 자체가 디버깅의 실마리를 담고 있었다. “20만 행은 되고 50만 행은 안 된다"는 건 데이터 크기의 문제가 아니라 처리 시간의 문제라는 뜻이다. 어딘가에 시간 기반 제한이 걸려 있다.

에러 로그 추적
#

먼저 Superset 서버 로그에서 해당 시점의 에러를 확인했다.

psycopg2.OperationalError: SSL connection has been closed unexpectedly

처음 이 에러를 봤을 때는 Redshift 커넥션이 끊긴 것으로 생각했다. Superset에서 CSV를 다운로드하려면 Redshift에서 데이터를 가져와야 하고, 50만 행이면 Redshift 쪽에서 타임아웃이 걸렸을 수 있다고 추정했다.

하지만 에러 로그를 자세히 보니, 이 psycopg2 에러는 Redshift 커넥션이 아니었다. Superset의 메타데이터 데이터베이스인 Aurora PostgreSQL 커넥션에서 발생한 에러였다. Redshift는 sqlalchemy-redshift 드라이버를 사용하고, 메타데이터 DB는 psycopg2를 사용한다.

왜 데이터를 가져오는 게 아닌 메타데이터 DB에서 에러가 날까?

원인 분석
#

CSV 다운로드 흐름을 코드 레벨에서 추적해보면 이렇다:

  1. 사용자가 CSV 다운로드 요청
  2. Superset이 메타데이터 DB(Aurora PostgreSQL)에 트랜잭션을 열고, 사용자의 다운로드 권한을 확인한다
  3. 권한 확인이 통과되면, 같은 요청 컨텍스트 안에서 Redshift로부터 실제 데이터를 가져오기 시작한다
  4. 50만 행 이상이면 Redshift에서 데이터를 가져오는 데 1분 이상 소요된다
  5. 이 동안 2번에서 열린 메타데이터 DB 쪽 트랜잭션은 아무 쿼리도 실행하지 않은 채 열린 상태로 idle 대기한다
  6. Aurora RDS의 idle_in_transaction_session_timeout 설정이 60,000ms(1분)로 되어 있다
  7. idle 상태의 트랜잭션이 1분을 초과하면 PostgreSQL이 해당 커넥션을 강제 종료한다
  8. 메타데이터 DB 커넥션이 끊기면서 Superset 서버가 500 에러를 반환한다

핵심은 5번이다. 권한 체크는 이미 끝났는데, 트랜잭션이 닫히지 않은 채로 열려 있었다. Superset의 SQLAlchemy 세션 관리에서 요청이 완료될 때까지 트랜잭션을 유지하는 구조 때문이다.

해당 시점의 Aurora RDS 에러 로그를 확인하니 이를 뒷받침하는 로그가 있었다.

master@superset:[17810]:FATAL: terminating connection due to idle-in-transaction timeout

RDS 파라미터 그룹에서 idle_in_transaction_session_timeout 값을 확인하니 60,000ms(1분)로 설정되어 있었다. 정확히 제보 내용과 맞는다. 20만 행은 1분 안에 완료되고, 50만 행은 1분을 넘기니까 실패하는 것이다.

대응
#

RDS 파라미터 그룹에서 idle_in_transaction_session_timeout을 60,000ms(1분)에서 600,000ms(10분)로 변경을 요청했다. Superset 웹서버의 다른 타임아웃 설정들 — ALB 타임아웃, gunicorn 워커 타임아웃 — 이 이미 10분(600초)으로 설정되어 있으므로, 메타데이터 DB의 타임아웃도 이에 맞추는 것이 합리적이다.

더 근본적인 해결 방법도 생각해봤다. 데이터 다운로드가 진행되는 동안 권한 체크용 트랜잭션이 열려 있을 필요가 없으니, Superset 코드를 수정해서 권한 확인 후 바로 트랜잭션을 커밋하는 방법이 있다. 또는 메타데이터 DB 앞에 PgBouncer 같은 커넥션 풀러를 두고 transaction pooling 모드로 운영하면 idle 트랜잭션 자체가 줄어든다.

하지만 이 두 방법은 코드 변경이나 아키텍처 변경이 필요하고, 영향 범위를 충분히 검증해야 한다. RDS 파라미터 변경은 기존 운영 구조를 건드리지 않으면서 즉시 적용 가능한 방법이었다. 우선 파라미터를 변경하고, 코드 레벨 개선은 별도 티켓으로 분리했다.

설정 변경 후 50만 행 이상의 CSV 다운로드도 정상 동작함을 확인했다.


기타: Helm 차트 db-init Job ARM 노드 이슈
#

사용자 제보 이슈와는 별개로, 운영 중 발견한 이슈도 하나 있었다.

Superset 베이스 Helm 차트에 포함된 db-init Job이 amd64 전용 이미지를 사용하고 있었다. 문제는 이 Job의 파드 템플릿에 노드 셀렉터가 설정되어 있지 않아서, Kubernetes 스케줄러가 ARM(Graviton) 노드에 배치하면 이미지 아키텍처 불일치로 실패한다는 것이다.

우리 EKS 클러스터는 비용 최적화를 위해 amd64와 arm64 노드를 혼용하고 있다. 노드 셀렉터가 없으면 어느 아키텍처 노드에 뜰지 모르기 때문에, amd64 전용 이미지는 확률적으로 실패한다. 배포할 때마다 되다가 안 되다가 해서 원인을 찾는 데 시간이 좀 걸렸다.

Helm 차트 버전을 v0.7.7에서 v0.8.10으로 올려서 해결했다. 새 버전에서는 db-init Job에도 아키텍처별 노드 셀렉터가 적용되도록 수정되어 있었다.


배운 것
#

“N만 행은 되고 M만 행은 안 된다"는 시간 기반 제한의 신호다. 행 수 자체가 문제라면 10만 행에서도 안 되어야 한다. 특정 행 수에서 경계가 생긴다면 처리 시간에 걸리는 타임아웃을 의심해야 한다. 이 관점이 없으면 데이터소스 커넥션(Redshift)만 계속 들여다보게 된다.

에러가 발생하는 커넥션과 원인이 되는 커넥션이 다를 수 있다. 이번 케이스에서 SSL 커넥션 에러가 발생한 곳은 Redshift가 아니라 메타데이터 DB(Aurora PostgreSQL)였다. 에러 메시지의 드라이버(psycopg2 vs sqlalchemy-redshift)를 확인해서 어느 커넥션인지 정확히 파악하는 것이 중요하다. 에러 메시지만 보고 “아 Redshift가 끊겼구나"라고 성급하게 결론내리면 방향을 완전히 잘못 잡는다.

타임아웃 설정은 시스템 전체에서 일관되게 맞춰야 한다. ALB 타임아웃이 10분, gunicorn 워커 타임아웃이 10분인데, 메타데이터 DB의 idle 트랜잭션 타임아웃만 1분이면, 체인의 가장 약한 고리에서 끊어진다. 분산 시스템에서 타임아웃 설정은 전체 경로를 따라가면서 일관성을 확인해야 한다.

CSV + 한글 + 윈도우 = BOM이 필수다. utf-8-sig는 Python에서 BOM을 삽입하는 가장 간단한 방법이다. 인코딩 파라미터 하나만 바꾸면 된다. 윈도우 Excel 사용자가 있는 환경이라면 CSV 내보내기에 기본으로 적용하는 것이 좋다. macOS나 Linux 사용자에게는 BOM이 있어도 없어도 상관없으니, BOM을 넣는 것이 안전한 기본값이다.

사용자 제보는 가장 가치 있는 QA다. 6건의 이슈 중 최소 3건(윈도우 CSV 한글 깨짐, 50만 행 CSV 다운로드 실패, Slack 파일 확장자)은 내부 테스트에서 발견하기 극히 어려웠을 것이다. 우리 팀은 주로 macOS 환경에서 테스트하고, 50만 행 이상의 CSV를 다운로드하는 시나리오는 테스트 케이스에 없었다. 실제 사용 패턴은 개발자의 테스트 시나리오와 다르다. 적극적으로 사용해주시고 이슈를 제보해주시는 사용자가 있다는 건 정말 감사한 일이다.

참고 자료: