[AWS] Glue + Athena로 S3 데이터를 SQL로 조회하기: 테이블 생성부터 JOIN·집계·검증까지

Yuchan Han·2026년 4월 5일

AWS

목록 보기
7/7

AWS에서 로그나 CSV 파일을 S3에 쌓아 두고, 별도 서버 없이 SQL로 바로 분석하고 싶은 경우가 많습니다. 서버를 띄우지 않아도 되는 서버리스 분석 조합이 바로 Glue Data Catalog + Athena입니다. Glue가 S3 파일의 스키마(테이블 정의)를 관리하고, Athena가 그 정의를 읽어서 SQL을 실행하는 구조입니다.

이번 글에서는 S3에 올린 CSV 두 개를 Glue에 테이블로 등록하고, Athena에서 기본 조회 → JOIN → 집계 → 검증까지 직접 실행합니다. 웹 콘솔 클릭 경로 중심으로 설명하고, 실습 중 자주 발생하는 오류와 해결법도 함께 다룹니다.


1. 실습 전 준비

실습을 시작하기 전에 아래 세 가지를 먼저 고정합니다. 리전, S3 경로, 데이터 형태가 맞지 않으면 이후 모든 단계에서 문제가 생깁니다.

1-1. 리전 통일

Athena, Glue, S3를 같은 리전에서 사용합니다. 이번 글은 ap-northeast-2(서울) 기준입니다.

⚠️ 세 서비스의 리전이 하나라도 다르면, Athena에서 테이블이 보이지 않거나 쿼리가 실행되지 않습니다. 콘솔 우측 상단의 리전 표시를 반드시 확인합니다.

1-2. S3 경로 고정

용도경로
로그 데이터s3://<bucket>/samplelog/
타이틀 매핑 데이터s3://<bucket>/titles/
Athena 쿼리 결과 저장s3://<bucket>/athena-results/

<bucket> 부분은 본인이 만든 버킷 이름으로 바꿉니다. 세 경로 모두 같은 버킷 안에 두면 관리가 편합니다.

1-3. 데이터 형태 확인

이번 실습에서 사용하는 CSV 파일은 두 개입니다.

samplelog.csv — 사용자 로그 테이블

title_id,country,device_type,device_ts
101,KR,mobile,2026-04-01 10:03:22
101,KR,tv,2026-04-01 10:12:10
102,US,web,2026-04-01 11:01:00
103,JP,mobile,2026-04-02 09:30:00
102,KR,mobile,2026-04-02 14:22:11

titles.csv — 타이틀 매핑 테이블

title_id,title
101,드라마A
102,예능B
103,영화C

두 파일 모두 첫 번째 줄이 헤더(컬럼명)입니다. S3에 업로드할 때 각각 samplelog/, titles/ 폴더 안에 넣습니다.

⚠️ 폴더 안에 CSV 파일만 있어야 합니다. 관계없는 파일이 섞여 있으면 Athena가 읽다가 오류를 냅니다.


2. 전체 흐름

작업 순서를 먼저 잡고 시작합니다.

순서작업도구
S3에 CSV 업로드S3 콘솔
Athena 결과 저장 위치 설정Athena 콘솔
Glue에서 Database 생성Glue 콘솔
Glue에서 테이블 2개 수동 생성Glue 콘솔
Athena에서 테이블 인식 확인Athena 쿼리
기본 조회 (LIMIT, COUNT)Athena 쿼리
JOIN + 집계 쿼리Athena 쿼리
결과 검증Athena 쿼리

비유하자면, Glue는 도서관의 카탈로그(목록표)이고 Athena는 그 목록표를 보고 실제 책(S3 데이터)을 찾아 읽는 사서입니다. 카탈로그가 틀리면 사서가 엉뚱한 책을 가져오거나 아예 찾지 못합니다.


3. Step 1 — Glue에서 Database와 테이블 생성

3-1. Athena 결과 저장 위치 먼저 설정

Athena에서 쿼리를 실행하려면, 결과를 저장할 S3 경로가 반드시 지정되어 있어야 합니다. 이 설정이 빠지면 쿼리 자체가 실행되지 않습니다.

  1. AWS 콘솔 → Athena 이동
  2. 좌측 메뉴에서 Query editor 클릭
  3. 상단 Settings 탭 클릭
  4. Manage 버튼 클릭
  5. Query result location 항목에 s3://<bucket>/athena-results/ 입력
  6. Save 클릭
확인 항목기대 결과
Query result location 값s3://<bucket>/athena-results/ 가 표시됨
Settings 저장 후 Query editor 복귀오류 메시지 없음

⚠️ 이 단계를 건너뛰면 이후 모든 쿼리에서 No output location provided 오류가 발생합니다. 처음 한 번만 설정하면 됩니다.

3-2. Glue Database 생성

Glue Database는 테이블을 묶어서 관리하는 이름 공간입니다.

  1. AWS 콘솔 → Glue 이동
  2. 왼쪽 메뉴 Data CatalogDatabases 클릭
  3. Add database 클릭
  4. Database name에 media_analytics 입력
  5. Create database 클릭
확인 항목기대 결과
Databases 목록media_analytics가 표시됨

3-3. samplelog 테이블 생성 (콘솔 수동)

Glue 테이블을 만드는 방법은 크게 두 가지입니다. Crawler를 돌려서 자동 생성하거나, 콘솔에서 수동으로 만들 수 있습니다. 이번 글에서는 수동 생성을 먼저 다룹니다. 수동으로 해 봐야 스키마가 어떻게 구성되는지 직접 확인할 수 있습니다.

  1. Glue 콘솔 → Data CatalogTables 이동
  2. Add table 클릭
  3. Add table manually 선택

아래 값을 순서대로 입력합니다.

항목
Table namesamplelog
Databasemedia_analytics (드롭다운에서 선택)
Data storeS3
S3 paths3://<bucket>/samplelog/ ← 마지막 슬래시 포함
Data formatCSV
Delimiter, (Comma)

다음 단계에서 컬럼을 추가합니다.

컬럼 이름타입설명
title_idint타이틀 식별자
countrystring국가 코드
device_typestring디바이스 종류
device_tsstring이벤트 발생 시각

device_ts를 string으로 두는 이유: timestamp 타입으로 설정하면 CSV의 날짜 포맷이 조금이라도 다를 때 파싱 오류가 납니다. string으로 먼저 등록하고, Athena 쿼리에서 DATE_PARSE 함수로 변환하는 편이 디버깅이 훨씬 쉽습니다.

마지막으로 Table properties에서 아래 항목을 반드시 설정합니다.

속성이유
skip.header.line.count1CSV 첫 줄(헤더)을 데이터로 읽지 않도록 건너뜀

⚠️ 이 속성을 빠뜨리면 title_id,country,device_type,device_ts라는 문자열이 첫 번째 데이터 행으로 잡힙니다. 조회할 때 숫자 컬럼에 문자열이 들어 있어서 CAST 오류가 발생합니다.

Create 클릭으로 테이블 생성을 완료합니다.

3-4. titles 테이블 생성

같은 방식으로 두 번째 테이블을 만듭니다.

항목
Table nametitles
Databasemedia_analytics
Data storeS3
S3 paths3://<bucket>/titles/
Data formatCSV
Delimiter,
컬럼 이름타입
title_idint
titlestring

Table properties에 skip.header.line.count = 1도 동일하게 설정합니다.

⚠️ title_id 타입은 반드시 samplelog와 동일하게 int로 맞춥니다. 한쪽이 int이고 다른 쪽이 string이면 JOIN 결과가 0건으로 나옵니다. 이 실수가 가장 흔합니다.


4. Step 2 — Athena 기본 조회

Glue에서 테이블을 만들었으면, Athena에서 제대로 인식되는지부터 확인합니다.

4-1. 테이블 인식 확인

Athena Query editor에서 아래 쿼리를 하나씩 실행합니다.

⚠️ Athena는 한 번에 SQL 1개만 실행할 수 있습니다. 세미콜론으로 구분해서 2개를 붙여 넣으면 Only one sql statement is allowed 오류가 발생합니다.

SHOW DATABASES;

결과에 media_analytics가 보여야 합니다.

SHOW TABLES IN media_analytics;

결과에 samplelog, titles 두 개가 보여야 합니다.

DESCRIBE media_analytics.samplelog;
DESCRIBE media_analytics.titles;
확인 항목기대 결과
samplelog 컬럼title_id(int), country(string), device_type(string), device_ts(string)
titles 컬럼title_id(int), title(string)
title_id 타입양쪽 모두 int

컬럼 타입이 다르면 Glue 콘솔에서 해당 테이블의 Edit schema로 들어가서 수정한 뒤 다시 확인합니다.

4-2. 데이터 샘플 조회

SELECT * FROM media_analytics.samplelog LIMIT 10;
SELECT * FROM media_analytics.titles LIMIT 10;

성공 판정 기준: 두 쿼리 모두 데이터 행이 출력되고, 헤더 행(title_id,country,...)이 데이터로 섞여 있지 않아야 합니다.

4-3. 건수 확인

SELECT COUNT(*) AS samplelog_cnt FROM media_analytics.samplelog;
SELECT COUNT(*) AS titles_cnt FROM media_analytics.titles;

S3에 올린 CSV 행 수(헤더 제외)와 일치하는지 비교합니다.

4-4. NULL/빈값 확인

데이터 품질을 빠르게 점검하는 쿼리입니다.

-- samplelog 테이블 NULL/빈값 점검
SELECT
  SUM(CASE WHEN title_id IS NULL THEN 1 ELSE 0 END) AS null_title_id,
  SUM(CASE WHEN country IS NULL OR country = '' THEN 1 ELSE 0 END) AS null_country,
  SUM(CASE WHEN device_type IS NULL OR device_type = '' THEN 1 ELSE 0 END) AS null_device_type,
  SUM(CASE WHEN device_ts IS NULL OR device_ts = '' THEN 1 ELSE 0 END) AS null_device_ts
FROM media_analytics.samplelog;

성공 판정 기준: 모든 값이 0이면 정상입니다. 0이 아니면 S3 원본 CSV를 열어서 빈 셀이나 누락된 값을 확인합니다.


5. Step 3 — JOIN과 집계 쿼리

기본 조회가 정상이면 두 테이블을 조인하고, 집계 쿼리를 실행합니다.

5-1. 기본 JOIN

samplelog의 title_id와 titles의 title_id를 기준으로 조인합니다.

-- samplelog + titles 기본 조인
SELECT
  s.title_id,
  t.title,
  s.country,
  s.device_type,
  s.device_ts
FROM media_analytics.samplelog s
JOIN media_analytics.titles t
  ON s.title_id = t.title_id
LIMIT 100;

성공 판정 기준: 결과가 1건 이상 나와야 합니다. 0건이면 title_id 타입 불일치를 의심합니다.

만약 한쪽 title_id가 string이고 다른 쪽이 int여서 0건이 나온다면, 아래처럼 CAST로 임시 해결할 수 있습니다. 하지만 근본적으로는 Glue 테이블 정의에서 타입을 통일하는 것이 맞습니다.

-- title_id 타입이 다를 때 임시 해결
SELECT s.title_id, t.title
FROM media_analytics.samplelog s
JOIN media_analytics.titles t
  ON CAST(s.title_id AS VARCHAR) = CAST(t.title_id AS VARCHAR)
LIMIT 20;

5-2. 타이틀별 조회 건수

-- 타이틀별 조회 건수 (내림차순)
SELECT
  t.title,
  COUNT(*) AS view_cnt
FROM media_analytics.samplelog s
JOIN media_analytics.titles t
  ON s.title_id = t.title_id
GROUP BY t.title
ORDER BY view_cnt DESC;

성공 판정 기준: 타이틀별로 숫자가 나오고, 합계가 samplelog 전체 건수와 같거나 작아야 합니다. titles에 없는 title_id는 JOIN에서 빠지기 때문에 합계가 작을 수 있습니다.

5-3. 국가별 건수

-- 국가별 건수
SELECT
  country,
  COUNT(*) AS cnt
FROM media_analytics.samplelog
GROUP BY country
ORDER BY cnt DESC;

5-4. 디바이스별 건수

-- 디바이스 유형별 건수
SELECT
  device_type,
  COUNT(*) AS cnt
FROM media_analytics.samplelog
GROUP BY device_type
ORDER BY cnt DESC;

5-5. 일자별 집계 (문자열 timestamp 처리)

device_ts 컬럼은 string 타입이므로, 날짜로 변환해야 일자별 집계가 가능합니다. Athena(Presto 엔진)에서는 DATE_PARSE 함수를 사용합니다.

먼저 변환이 정상적으로 되는지 확인합니다.

-- 날짜 변환 테스트
SELECT
  device_ts,
  DATE_PARSE(device_ts, '%Y-%m-%d %H:%i:%s') AS parsed_ts
FROM media_analytics.samplelog
LIMIT 10;

변환이 정상이면 일자별 집계를 실행합니다.

-- 일자별 건수
SELECT
  DATE(DATE_PARSE(device_ts, '%Y-%m-%d %H:%i:%s')) AS dt,
  COUNT(*) AS cnt
FROM media_analytics.samplelog
GROUP BY DATE(DATE_PARSE(device_ts, '%Y-%m-%d %H:%i:%s'))
ORDER BY dt;

⚠️ DATE_PARSE의 포맷 문자열은 실제 데이터 형태와 정확히 일치해야 합니다. 예를 들어 데이터가 2026/04/01 10:03:22 형태인데 포맷을 %Y-%m-%d로 쓰면 파싱 오류가 납니다.

특정 날짜 필터링이 필요한 경우, 문자열 함수를 사용하는 방법도 있습니다. device_ts가 string 타입일 때는 substr이나 split_part가 더 간단합니다.

-- 특정 날짜 필터 (방법 1: substr)
SELECT *
FROM media_analytics.samplelog
WHERE substr(device_ts, 1, 10) = '2026-04-01';
-- 특정 날짜 필터 (방법 2: split_part)
SELECT *
FROM media_analytics.samplelog
WHERE split_part(device_ts, ' ', 1) = '2026-04-01';

⚠️ WHERE date(device_ts) = date '2026-04-01'처럼 stringdate로 직접 캐스팅하면 INVALID_CAST_ARGUMENT 오류가 발생합니다. device_ts가 string 타입인 경우, DATE_PARSE를 거치거나 문자열 함수로 비교해야 합니다.


6. Step 4 — 결과 검증

분석 쿼리를 돌린 뒤, 결과가 맞는지 확인하는 루틴입니다. 아래 순서대로 점검하면 "쿼리는 돌아가는데 결과가 이상한" 상태를 빠르게 잡아낼 수 있습니다.

순서검증 항목쿼리/방법
1샘플 10건 조회SELECT * FROM ... LIMIT 10 — 컬럼명, 값 형태 확인
2전체 건수COUNT(*) — S3 원본 행 수와 비교
3NULL/빈값위 4-4 쿼리 — 0이 아니면 원본 확인
4JOIN 매핑기본 JOIN 결과가 1건 이상인지 확인
5타이틀별 집계합계가 전체 건수 이하인지 확인
6국가/디바이스 집계합계가 전체 건수와 동일한지 확인
7일자별 집계날짜 범위가 기대한 기간과 맞는지 확인

최종 성공 판정 기준:

  • samplelog, titles 모두 LIMIT 10 조회 정상
  • 헤더가 데이터로 섞여 들어오지 않음
  • JOIN 결과가 0건이 아님
  • 국가/디바이스/일자 집계가 오류 없이 실행됨
  • 집계 합계가 전체 건수와 일치하거나, JOIN 누락분만큼만 차이남

7. 자주 틀리는 포인트

실습 중 자주 발생하는 오류를 표로 정리합니다.

증상원인해결
No output location provided — 쿼리 실행 자체가 안 됨Athena 결과 저장 위치 미설정Athena Settings → Query result location에 S3 경로 입력
Only one sql statement is allowedSQL 2개를 세미콜론으로 이어 붙여 실행한 번에 1개씩 실행
INVALID_CAST_ARGUMENT / TYPE_MISMATCH — 날짜 필터에서 발생string 컬럼을 date로 직접 캐스팅DATE_PARSE 또는 substr/split_part 사용
JOIN 결과 0건양쪽 테이블의 title_id 타입 불일치 (한쪽 int, 한쪽 string)Glue 테이블 정의에서 타입 통일. 임시로는 CAST(... AS VARCHAR)
EXPRESSION_NOT_AGGREGATE — GROUP BY 오류SELECT에 있는 비집계 컬럼을 GROUP BY에 안 넣음SELECT의 비집계 컬럼을 GROUP BY에 모두 포함
헤더가 첫 번째 데이터 행으로 잡힘Glue 테이블의 skip.header.line.count 미설정Table properties에서 skip.header.line.count = 1 추가
DATE_PARSE 오류포맷 문자열과 실제 데이터 형태 불일치데이터를 LIMIT 10으로 먼저 확인하고 포맷을 정확히 맞춤
테이블이 Athena에서 안 보임리전 불일치 (Glue와 Athena가 다른 리전)S3, Glue, Athena 리전을 모두 동일하게 맞춤
Crawler 실행 후 테이블이 안 생김S3 입력 경로에 파일이 없음S3 경로에 CSV가 실제로 존재하는지 먼저 확인

8. 최종 체크리스트

실습을 마치기 전에 아래 항목을 하나씩 점검합니다.

#확인 항목상태
1S3, Glue, Athena의 리전이 모두 동일하다
2Athena 결과 저장 위치(athena-results/)가 설정되어 있다
3Glue database media_analytics가 생성되어 있다
4Glue table samplelog, titles 2개가 생성되어 있다
5양쪽 테이블의 title_id 타입이 동일하다 (int)
6skip.header.line.count = 1이 양쪽 테이블 모두 설정되어 있다
7SELECT * LIMIT 10 조회에서 헤더가 데이터로 섞이지 않는다
8COUNT(*) 결과가 S3 원본 행 수와 일치한다
9JOIN 결과가 0건이 아니다
10GROUP BY 쿼리(국가/디바이스/일자)가 오류 없이 실행된다

마무리

Glue + Athena의 핵심은 복잡한 기능이 아니라 기본 정합성입니다. 리전 통일, S3 경로 확인, 스키마 타입 통일, 결과 저장 위치 설정 — 이 네 가지만 먼저 잡으면 대부분의 문제는 쿼리를 쓰기도 전에 예방됩니다.

profile
맛있는거먹고싶어요

0개의 댓글