[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 Catalog → Databases 클릭
  3. Add database 클릭
  4. Database name에 media_analytics 입력
  5. Create database 클릭
확인 항목기대 결과
Databases 목록media_analytics가 표시됨

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

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

  1. Glue 콘솔 → Data Catalog → Tables 이동
  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'처럼 string을 date로 직접 캐스팅하면 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개의 댓글