쏘카
경력근거: Product Engineer가 안전하게 셀프서비스로 수행할 수 있는 자동화된 가드레일과 도구로 전환합니다
※ 실제 산출물이 아닌 AI 예측 시안입니다.
쏘카 Tech Platform 은 한 명의 Product Engineer 가 제품 기능을 전 스택으로 책임지는 조직으로 옮겨 가고 있고, 공고는 이 자리를 '티켓 자체가 필요 없게 만드는 엔지니어' 로 적었습니다. 이 플랫폼 팀이 만드는 제품의 사용자는 사내 Product Engineer 이고, 데이터베이스 쪽 과제는 '스키마 변경·성능 튜닝·백업/복구·프로비저닝을 Product Engineer가 안전하게 셀프서비스로 수행할 수 있는 자동화된 가드레일과 도구로 전환합니다' 로 명시돼 있습니다. 티켓이 사라지면 DBA 가 티켓을 받으며 하던 확인, 곧 이 ALTER 가 쓰기를 멈추는지, 지울 컬럼을 아직 누가 읽는지, 되돌릴 스냅샷이 있는지를 PR 앞에서 누군가 대신 봐야 합니다. 공고가 'Aurora MySQL을 중심으로' 운영한다고 적고 우대사항에 '무중단 스키마 변경(온라인 DDL)' 과 'Internal Developer Platform 또는 플랫폼-as-product 관점' 을 든 것이 판정 기준을 Aurora MySQL 3 의 온라인 DDL 표로 세운 이유이고, '모든 변경은 PR로 관리되고' 라는 일하는 방식이 판정을 PR 에 다는 이유입니다. 이 포트폴리오는 DDL 문장을 작업 종류로 읽어 공개 온라인 DDL 표와 테이블 통계·조회 기록·스냅샷 시각에 견주는 판정 규칙을 보여줍니다. 대형 Aurora 클러스터를 직접 운영해 온 DBA 의 운영 깊이는 주장하지 않습니다.
공고는 DBA 와 DevOps 가 손으로 처리하던 일을 Product Engineer 가 직접 셀프서비스로 수행하는 플랫폼으로 다시 설계한다고 적었습니다. 스키마 변경 PR 을 티켓 없이 올리는 Product Engineer 가 이 CLI 의 사용자이고, 그 사람이 머지 전에 스스로 판단할 근거가 판정 한 줄입니다.
주요 업무가 Aurora MySQL 을 중심으로 스키마 변경·성능 튜닝·백업/복구를 자동화된 가드레일로 바꾸는 일이고, 우대사항에 무중단 스키마 변경(온라인 DDL)이 따로 있습니다. 쓰기 잠금 판정은 온라인 DDL 표를, 백업 판정은 스냅샷 시각을 읽어 이 두 줄을 한 도구에서 다룹니다.
일하는 방식에 모든 변경은 PR 로 관리되고 인프라는 셀프서비스가 원칙이라고 적혀 있고, 우대사항은 규정을 정책 엔진으로 코드화한 경험을 듭니다. 3주차에 판정을 PR 체크로 달고 세 규칙을 Rego 정책으로 옮기는 단계가 이 줄에 닿습니다.
Product Engineer 가 스키마 변경 PR 을 올리면 마이그레이션 SQL 을 문장마다 읽어 ALTER 절이 어떤 작업인지(컬럼 추가·타입 변경·인덱스 추가 등) 가립니다. 그 작업을 MySQL 8.0 온라인 DDL 표에서 찾아 쓰기를 막는지 보고, 대상 테이블이 100만 행을 넘는지 봅니다. 컬럼을 지우는 문장은 최근 7일 쿼리 다이제스트에서 그 컬럼이 아직 읽히는지 보고, 컬럼·테이블을 지우거나 비우는 문장은 그 클러스터의 마지막 수동 스냅샷이 24시간 안에 찍혔는지 봅니다. 문장마다 판정 하나와 알고리즘·행 수·조회 수·스냅샷 경과 시간이 터미널과 JSON 으로 나오므로, Product Engineer 는 DBA 에게 묻지 않고 어느 문장을 고칠지 압니다. 엔진은 Aurora MySQL 3(MySQL 8.0 호환)으로 전제합니다.
인프라팀에 티켓을 넣지 않고 스키마 변경 PR 을 직접 올리는 쏘카 Product Engineer — DBA 검토 없이 머지해도 되는지 PR 파이프라인이 알려 주지 않으면 스스로 판단할 근거가 없다
Product Engineer 가 스키마 변경 앞에서 머뭇거리는 까닭은 SQL 을 몰라서가 아닙니다. 같은 ALTER 라도 작업 종류와 테이블 크기에 따라 몇 초 만에 끝나기도 하고 수십 분 동안 쓰기를 막기도 하는데, 그 차이가 PR 화면에는 드러나지 않습니다. 작업 종류를 공개 문서의 온라인 DDL 표와 맞추고 테이블 통계·조회 기록·스냅샷 시각을 곁들이면, 티켓을 받던 DBA 가 머릿속으로 하던 세 가지 확인이 문장마다 판정으로 나옵니다.
Product Engineer 가 PR 번호와 클러스터를 주면 마이그레이션 SQL 문장마다 네 판정 중 하나와 알고리즘·행 수·7일 조회 수·스냅샷 경과 시간을 터미널 표와 JSON 으로 받는 CLI 가 도는 완결품입니다. 테이블 통계와 쿼리 다이제스트는 로컬 MySQL 8.0 컨테이너에서 내보낸 파일로, 스냅샷 목록은 AWS CLI 응답 모양의 파일로 받아 AWS 계정 없이 돕니다.
2주차부터 AWS 에 OpenTofu 로 Aurora MySQL 3 시험 클러스터를 띄우고, 쓰기 부하를 건 채 표의 작업 12종을 돌려 쓰기가 멈춘 작업을 표의 locks_writes 와 맞대어 봅니다. 손으로 정답을 단 60문장으로 판정 일치율도 냅니다.
GitHub Actions 가 PR 마다 CLI 를 돌려 판정을 코멘트로 달고, 바로 머지 가능이 아닌 문장이 하나라도 있으면 체크를 실패로 둡니다. Product Engineer 는 PR 화면에서 어느 문장이 왜 걸렸는지 읽고, 같은 CLI 를 Claude Code 가 부르는 도구로도 씁니다.
Product Engineer 가 `ddl-guard check --pr 1482 --cluster core-aurora` 를 치면 네 문장이 각각 판정을 받고, 문장마다 알고리즘·행 수·7일 조회 수·스냅샷 경과 시간이 붙어 나옵니다. PR 4건 12문장 전체로 이 흐름을 시연합니다.
기준값에 딱 맞는 입력에서 판정이 흔들리지 않습니다. 행 수가 정확히 100만인 테이블, 스냅샷이 정확히 24시간 전인 클러스터, 7일 호출이 0회인 컬럼은 모두 통과 쪽으로 떨어집니다.
세미콜론 없이 이어 붙인 문장, 주석 안의 DROP TABLE, 닫히지 않은 따옴표처럼 망가뜨린 마이그레이션 파일 30종을 넣어도 판정을 지어내지 않고, 읽지 못한 문장은 종료 코드 2로 멈춥니다.
2주차에 손으로 정답을 단 60문장에서 판정별 일치율을 냅니다. 60문장과 정답이 모두 합성이라, 이 값이 말하는 것은 제가 옮긴 온라인 DDL 표와 세 기준값이 코드에 어긋남 없이 들어갔는지까지입니다.
위 수치는 더미데이터 자가채점입니다. 머지 전에 걸러 낸 변경이 쏘카 운영 클러스터의 쓰기 멈춤을 얼마나 줄였는지는 Tech Platform 의 실제 마이그레이션 PR 과 클러스터 지표가 있어야 잽니다. 내보내기 파일 셋(테이블 통계·다이제스트·스냅샷 목록)을 운영 값으로 바꿔 끼우면 같은 명령이 그대로 돕니다.
PR 문장과 테이블 통계, 다이제스트, 스냅샷 목록을 모두 제가 만들었고 정답도 제가 매겼습니다. 제 더미에서 판정이 맞는 것은 제 규칙을 제가 확인한 데 그치고, 쏘카 클러스터에서 그 ALTER 가 실제로 쓰기를 멈추는지는 알려 주지 못합니다. 면접에서 근거로 내놓을 수 있는 것은 2주차 시험 클러스터에 부하를 걸고 잰 잠금 결과입니다.
모든 문장이 기준값에서 멀리 떨어져 있으면 100만 행 '초과' 를 '이상' 으로 잘못 짜도 판정이 그대로 나옵니다. 행 수가 정확히 100만인 테이블, 정확히 24시간 전 스냅샷, 7일 호출 0회 컬럼, 스냅샷을 한 번도 찍지 않은 클러스터, 다른 클러스터에만 조회가 남은 같은 이름 컬럼, 소문자·백틱 문장, 절이 둘 든 ALTER 를 더미에 일부러 넣은 이유입니다.
스냅샷 경과 시간은 PR 을 올린 시각(requested_at)에서 잽니다. PR 이 며칠 열려 있다가 머지되면 그사이 스냅샷이 24시간을 넘겨도 판정은 올린 날 기준으로 남으므로, 실제 도입 때는 PR 체크가 머지 직전 시각을 requested_at 자리에 넣어 다시 돌려야 합니다. 이 더미는 PR 을 올린 시각만 담습니다.
잠금 없음으로 판정한 변경도 시작과 끝에 메타데이터 잠금을 잠깐 잡습니다. 그 순간 긴 트랜잭션이 테이블을 쥐고 있으면 ALTER 가 기다리고 뒤따르는 쓰기도 줄지어 멈추는데, 이 도구는 그 대기를 보지 않습니다. 적용 직전 information_schema.INNODB_TRX 를 보는 일은 도입 단계 과제로 둡니다.
TABLE_ROWS 는 InnoDB 통계에서 나온 추정치라 실제 행 수와 수십 퍼센트까지 벌어질 수 있습니다. 100만 행 근처 테이블의 판정은 뒤집힐 수 있어, 기준값에 가까운 테이블은 읽기 전용 복제본에서 COUNT(*) 로 다시 재는 단계를 3주차 이후에 붙입니다.
파서는 현재 컬럼 타입을 모르므로 MODIFY·CHANGE 를 모두 타입 변경(COPY)으로 읽습니다. VARCHAR 길이만 늘리는 변경은 실제로 INPLACE 인데 쓰기 잠금으로 판정될 수 있고, information_schema.COLUMNS 를 입력에 더해야 이 과잉 경고가 사라집니다.
SELECT * 로 읽는 쿼리는 다이제스트에 컬럼 이름이 나오지 않아 query_digest 에 잡히지 않습니다. ORM 이 SELECT * 를 쓰는 테이블에서는 아직 읽히는 컬럼을 지워도 이 도구가 조용할 수 있어, 그런 테이블은 실제 도입 때 다이제스트 수집 방식부터 다시 정합니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
Product Engineer 가 올린 스키마 변경 PR 의 마이그레이션 SQL 을 문장 하나당 한 행으로 둡니다. 요청 번호와 PR 번호, 대상 클러스터와 테이블, SQL 원문, PR 을 올린 시각(KST), 작성자를 담습니다. 지원자가 자기 GitHub 저장소에 마이그레이션 PR 을 올리고 diff 에서 문장을 뽑아 만듭니다. 소문자로 쓴 문장, 백틱과 스키마 이름이 붙은 테이블, 한 ALTER 에 절이 둘 들어간 문장을 섞어 파서가 문장 모양에 흔들리지 않는지 보게 둡니다.
| request_no | pr_no | cluster | table_name | ddl_statement | requested_at | author |
|---|---|---|---|---|---|---|
| SCR-1482-1 | 1,482 | core-aurora | car_reservations | ALTER TABLE car_reservations MODIFY me… | 2026-09-27T10:20:00+09:00 | jiwoo.han |
| SCR-1482-2 | 1,482 | core-aurora | members | ALTER TABLE members DROP COLUMN legacy… | 2026-09-27T10:20:00+09:00 | jiwoo.han |
| SCR-1482-3 | 1,482 | core-aurora | coupon_issue_2023 | DROP TABLE coupon_issue_2023 | 2026-09-27T10:20:00+09:00 | jiwoo.han |
| SCR-1482-4 | 1,482 | core-aurora | vehicles | ALTER TABLE vehicles ADD COLUMN batter… | 2026-09-27T10:20:00+09:00 | jiwoo.han |
| SCR-1490-1 | 1,490 | payment-aurora | payment_logs | alter table `socar_pay`.`payment_logs`… | 2026-09-27T14:05:00+09:00 | seoyeon.lim |
아래 JSON Schema 를 그대로 만족하는 스키마 변경 요청 행을 120개 더 만들어 주세요. PR 을 서른 건쯤 두고 PR 마다 문장을 1~6개 답니다. 한 PR 의 문장은 같은 cluster 와 같은 requested_at 을 씁니다. 문장 종류는 ADD COLUMN, DROP COLUMN, MODIFY, ADD INDEX, ADD FULLTEXT INDEX, CONVERT TO CHARACTER SET, ALTER COLUMN SET DEFAULT, DROP TABLE, TRUNCATE TABLE 을 고르게 섞으세요. 쉬운 데이터만 만들지 말고 다음을 일부러 넣습니다: 열 문장에 하나꼴로 소문자 SQL, 백틱과 스키마 이름이 붙은 테이블, 한 ALTER 에 절을 둘 이상 넣은 문장. 형식을 일부러 깨뜨린 파일도 5건 만들되 세미콜론 없이 두 문장을 이어 붙이거나, 주석 안에 DROP TABLE 을 넣거나, 따옴표가 닫히지 않은 DEFAULT 값을 적는 식으로 만드세요. 판정 결과나 잠금 여부는 넣지 마세요.
{"type": "object", "properties": {"request_no": {"type": "string"}, "pr_no": {"type": "integer"}, "cluster": {"type": "string"}, "table_name": {"type": "string"}, "ddl_statement": {"type": "string"}, "requested_at": {"type": "string"}, "author": {"type": "string"}}, "required": ["request_no", "pr_no", "cluster", "table_name", "ddl_statement", "requested_at", "author"]}{"type": "object", "properties": {"request_no": {"type": "string"}, "pr_no": {"type": "integer"}, "cluster": {"type": "string"}, "table_name": {"type": "string"}, "ddl_statement": {"type": "string"}, "requested_at": {"type": "string"}, "author": {"type": "string"}}, "required": ["request_no", "pr_no", "cluster", "table_name", "ddl_statement", "requested_at", "author"]}클러스터마다 테이블 행 수와 데이터 크기를 둡니다. information_schema.TABLES 의 TABLE_ROWS 와 DATA_LENGTH 를 매일 새벽에 내보낸 모양이고, 지원자는 로컬 MySQL 8.0 컨테이너에 sysbench 로 채운 테이블에서 같은 쿼리를 돌려 실물을 만듭니다. 행 수가 정확히 100만인 테이블을 넣어 기준값 비교가 '넘으면' 을 지키는지 보게 둡니다.
| stat_no | cluster | table_name | row_count | data_size_gb | collected_at |
|---|---|---|---|---|---|
| TS-01 | core-aurora | car_reservations | 18,400,000 | 21.6 | 2026-09-27T06:00:00+09:00 |
| TS-02 | core-aurora | members | 4,120,000 | 3.8 | 2026-09-27T06:00:00+09:00 |
| TS-03 | core-aurora | coupon_issue_2023 | 9,850,000 | 6.2 | 2026-09-27T06:00:00+09:00 |
| TS-04 | core-aurora | vehicles | 24,310 | 0.02 | 2026-09-27T06:00:00+09:00 |
| TS-05 | core-aurora | promo_banners | 3,200 | 0.004 | 2026-09-27T06:00:00+09:00 |
아래 JSON Schema 를 그대로 만족하는 테이블 통계 행을 80개 더 만들어 주세요. 클러스터를 다섯 개쯤 두고 클러스터마다 테이블을 10~20개 답니다. row_count 는 수천에서 수천만까지 고르게 흩고, data_size_gb 는 행 수에 대략 비례하게 둡니다. 쉬운 데이터만 만들지 말고 row_count 가 정확히 1000000 인 테이블과 999999·1000001 인 테이블을 하나씩 넣고, 행이 0 인 빈 테이블도 두 개 넣으세요. collected_at 은 같은 날 새벽 시각으로 둡니다.
{"type": "object", "properties": {"stat_no": {"type": "string"}, "cluster": {"type": "string"}, "table_name": {"type": "string"}, "row_count": {"type": "integer", "minimum": 0}, "data_size_gb": {"type": "number", "minimum": 0}, "collected_at": {"type": "string"}}, "required": ["stat_no", "cluster", "table_name", "row_count", "data_size_gb", "collected_at"]}{"type": "object", "properties": {"stat_no": {"type": "string"}, "cluster": {"type": "string"}, "table_name": {"type": "string"}, "row_count": {"type": "integer", "minimum": 0}, "data_size_gb": {"type": "number", "minimum": 0}, "collected_at": {"type": "string"}}, "required": ["stat_no", "cluster", "table_name", "row_count", "data_size_gb", "collected_at"]}MySQL 8.0 공식 문서의 온라인 DDL 지원표(15.12.1 Online DDL Operations)를 작업 종류당 한 행으로 옮긴 기준표입니다. Aurora MySQL 3 은 MySQL 8.0 호환 엔진이라 이 표를 그대로 따르되, 3.04(8.0.28 호환) 이하 버전이 섞여 있다고 보고 8.0.29 이후에 INSTANT 가 된 작업은 INPLACE 로 둡니다. 판정은 이 표의 locks_writes 만 읽고, algorithm 과 rebuilds_table 은 PR 코멘트에 보여 줄 값입니다.
| matrix_no | operation | clause_pattern | algorithm | rebuilds_table | locks_writes | note | source_doc |
|---|---|---|---|---|---|---|---|
| ODM-01 | add_column | ADD COLUMN | INSTANT | false | false | 끝자리에 붙이는 컬럼은 8.0.12 부터 INSTANT. Aurora… | MySQL 8.0 Reference Manual 15.12.1 Onl… |
| ODM-02 | drop_column | DROP COLUMN | INPLACE | true | false | 8.0.29 부터 INSTANT 도 되지만 Aurora MySQL 3… | MySQL 8.0 Reference Manual 15.12.1 Onl… |
| ODM-03 | rename_column | RENAME COLUMN / CHANGE 같은 타입 | INPLACE | false | false | 8.0.28 부터 INSTANT 가능 | MySQL 8.0 Reference Manual 15.12.1 Onl… |
| ODM-04 | change_column_type | MODIFY / CHANGE 타입 변경 | COPY | true | true | 테이블을 복사하는 동안 쓰기 불가. 타입이 같은지 알 수 없는 MOD… | MySQL 8.0 Reference Manual 15.12.1 Onl… |
| ODM-05 | set_column_default | ALTER COLUMN … SET DEFAULT | INSTANT | false | false | 메타데이터만 바뀜 | MySQL 8.0 Reference Manual 15.12.1 Onl… |
아래 JSON Schema 를 그대로 만족하는 온라인 DDL 작업 행을 MySQL 8.0 Reference Manual 15.12.1 의 표에서 더 옮겨 주세요. 파티션 작업, 테이블 옵션(ROW_FORMAT·KEY_BLOCK_SIZE), 컬럼 NULL 허용 변경, VIRTUAL 컬럼 추가처럼 아직 없는 작업을 대상으로 합니다. 문서가 '*' 로 조건을 단 칸은 Aurora MySQL 3.04(8.0.28 호환) 기준으로 보수적인 쪽을 고르고 그 조건을 note 에 적으세요. 문서에 없는 작업을 지어내지 마세요.
{"type": "object", "properties": {"matrix_no": {"type": "string"}, "operation": {"type": "string"}, "clause_pattern": {"type": "string"}, "algorithm": {"type": "string", "enum": ["INSTANT", "INPLACE", "COPY"]}, "rebuilds_table": {"type": "boolean"}, "locks_writes": {"type": "boolean"}, "note": {"type": "string"}, "source_doc": {"type": "string"}}, "required": ["matrix_no", "operation", "clause_pattern", "algorithm", "rebuilds_table", "locks_writes", "note", "source_doc"]}{"type": "object", "properties": {"matrix_no": {"type": "string"}, "operation": {"type": "string"}, "clause_pattern": {"type": "string"}, "algorithm": {"type": "string", "enum": ["INSTANT", "INPLACE", "COPY"]}, "rebuilds_table": {"type": "boolean"}, "locks_writes": {"type": "boolean"}, "note": {"type": "string"}, "source_doc": {"type": "string"}}, "required": ["matrix_no", "operation", "clause_pattern", "algorithm", "rebuilds_table", "locks_writes", "note", "source_doc"]}클러스터·테이블·컬럼마다 최근 7일 동안 그 컬럼을 읽은 쿼리 호출 수를 둡니다. performance_schema.events_statements_summary_by_digest 의 COUNT_STAR 를 7일 간격으로 두 번 떠서 뺀 값이고, DIGEST_TEXT 에 이름이 나온 컬럼마다 한 행입니다. 지원자는 로컬 MySQL 8.0 에 sysbench 와 직접 짠 조회를 흘려 같은 표를 만듭니다. 이름이 같은 테이블·컬럼이 다른 클러스터에만 조회가 남은 행과 7일 호출이 0회인 행을 넣어 클러스터까지 맞춰 보는지 보게 둡니다.
| digest_no | cluster | table_name | column_name | calls_7d | digest_text | last_seen_at |
|---|---|---|---|---|---|---|
| QD-01 | core-aurora | members | legacy_grade | 3,212 | SELECT `id` , `legacy_grade` FROM `mem… | 2026-09-27T09:58:00+09:00 |
| QD-02 | core-aurora | members | grade | 88,410 | SELECT `grade` FROM `members` WHERE `i… | 2026-09-27T10:02:00+09:00 |
| QD-03 | core-aurora | car_reservations | memo | 1,204 | SELECT `memo` FROM `car_reservations` … | 2026-09-27T09:41:00+09:00 |
| QD-04 | core-aurora | vehicles | plate_no | 240,112 | SELECT `plate_no` , `zone_id` FROM `ve… | 2026-09-27T10:05:00+09:00 |
| QD-05 | payment-aurora | refund_requests | old_reason | 0 | SELECT `old_reason` FROM `refund_reque… | 2026-08-11T16:22:00+09:00 |
아래 JSON Schema 를 그대로 만족하는 다이제스트 행을 150개 더 만들어 주세요. digest_text 는 MySQL 다이제스트 모양(값 자리는 ?, 이름은 백틱)으로 쓰고 column_name 은 그 문장에 실제로 나온 컬럼만 고릅니다. calls_7d 는 0 부터 수십만까지 고르게 흩되 스무 행에 하나꼴로 0 을 넣고 last_seen_at 을 한 달 이상 전으로 둡니다. 쉬운 데이터만 만들지 말고 같은 table_name·column_name 을 서로 다른 cluster 에 두 행 넣어, 한 클러스터에서는 호출이 있고 다른 클러스터에서는 0 이 되게 하세요. SELECT * 다이제스트는 컬럼 이름이 없으므로 행으로 만들지 마세요.
{"type": "object", "properties": {"digest_no": {"type": "string"}, "cluster": {"type": "string"}, "table_name": {"type": "string"}, "column_name": {"type": "string"}, "calls_7d": {"type": "integer", "minimum": 0}, "digest_text": {"type": "string"}, "last_seen_at": {"type": "string"}}, "required": ["digest_no", "cluster", "table_name", "column_name", "calls_7d", "digest_text", "last_seen_at"]}{"type": "object", "properties": {"digest_no": {"type": "string"}, "cluster": {"type": "string"}, "table_name": {"type": "string"}, "column_name": {"type": "string"}, "calls_7d": {"type": "integer", "minimum": 0}, "digest_text": {"type": "string"}, "last_seen_at": {"type": "string"}}, "required": ["digest_no", "cluster", "table_name", "column_name", "calls_7d", "digest_text", "last_seen_at"]}클러스터마다 가장 최근 수동 클러스터 스냅샷의 id 와 생성 시각(UTC)을 둡니다. aws rds describe-db-cluster-snapshots --snapshot-type manual 응답에서 클러스터별 최신 한 건을 고른 모양이고, 지원자는 자기 AWS 계정의 시험 클러스터로 같은 응답을 받습니다. 새로 만든 fleet-aurora 는 수동 스냅샷을 한 번도 찍지 않아 id 와 시각이 null 로 비어 있습니다. PR 시각은 KST, 스냅샷 시각은 UTC 라 경과 시간은 한 시간대로 옮긴 뒤 잽니다.
| backup_no | cluster | engine_version | last_snapshot_id | last_snapshot_at |
|---|---|---|---|---|
| BK-01 | core-aurora | 8.0.mysql_aurora.3.04.1 | core-aurora-pre-pr1477 | 2026-09-25T18:20:00Z |
| BK-02 | payment-aurora | 8.0.mysql_aurora.3.05.2 | payment-aurora-pre-pr1488 | 2026-09-26T05:05:00Z |
| BK-03 | fleet-aurora | 8.0.mysql_aurora.3.08.0 | — | — |
| BK-04 | plan-aurora | 8.0.mysql_aurora.3.05.2 | plan-aurora-pre-pr1462 | 2026-09-26T23:40:00Z |
| BK-05 | parking-aurora | 8.0.mysql_aurora.3.04.1 | parking-aurora-pre-pr1470 | 2026-09-24T02:10:00Z |
아래 JSON Schema 를 그대로 만족하는 클러스터 백업 행을 30개 더 만들어 주세요. cluster 는 '<업무>-aurora' 이름으로 두고 engine_version 은 8.0.mysql_aurora.3.04.1·3.05.2·3.08.0 중에서 고릅니다. last_snapshot_at 은 UTC 의 Z 표기로 쓰고 기준 시각 2026-09-27T05:00:00Z 에서 1시간 전부터 열흘 전까지 흩으세요. 쉬운 데이터만 만들지 말고 기준 시각에서 정확히 24시간 전인 행 하나와, 스냅샷을 한 번도 찍지 않아 last_snapshot_id·last_snapshot_at 이 둘 다 null 인 행 둘을 넣으세요.
{"type": "object", "properties": {"backup_no": {"type": "string"}, "cluster": {"type": "string"}, "engine_version": {"type": "string"}, "last_snapshot_id": {"type": ["string", "null"]}, "last_snapshot_at": {"type": ["string", "null"]}}, "required": ["backup_no", "cluster", "engine_version", "last_snapshot_id", "last_snapshot_at"]}{"type": "object", "properties": {"backup_no": {"type": "string"}, "cluster": {"type": "string"}, "engine_version": {"type": "string"}, "last_snapshot_id": {"type": ["string", "null"]}, "last_snapshot_at": {"type": ["string", "null"]}}, "required": ["backup_no", "cluster", "engine_version", "last_snapshot_id", "last_snapshot_at"]}| Python | sqlglot 으로 마이그레이션 SQL 을 문장·ALTER 절 단위로 자르고 작업 종류를 뽑는 파서와 세 판정 규칙, CLI 를 맡습니다. |
| Aurora MySQL | 판정 기준표를 Aurora MySQL 3(MySQL 8.0 호환)의 온라인 DDL 동작으로 세우고, 2주차에 시험 클러스터에서 ALTER 를 돌려 표의 잠금 값과 실제 동작을 맞대어 봅니다. |
| AWS | describe-db-cluster-snapshots 응답으로 클러스터별 마지막 수동 스냅샷을 읽고, 2주차부터 시험 클러스터를 AWS 에 띄워 운영합니다. |
| OpenTofu | 시험용 Aurora 클러스터와 파라미터 그룹(performance_schema 켜기), 수동 스냅샷을 코드로 띄우고 내립니다. |
| CI/CD | 3주차에 GitHub Actions 가 PR 마다 CLI 를 돌리고, 바로 머지 가능이 아닌 문장이 있으면 체크를 실패로 둡니다. |
| OPA | 3주차에 세 판정 규칙을 Rego 정책으로 옮겨 conftest 로 같은 판정이 나오는지 회귀로 봅니다. |
| Claude Code | 3주차에 CLI 를 Claude Code 가 부르는 도구로 등록해 Product Engineer 가 마이그레이션을 쓰는 자리에서 먼저 판정을 받습니다. |
✗ fail → 읽지 못하는 문장이 열에 하나를 넘으면 작업 종류를 늘리기 전에 ALTER 절 자르기부터 다시 봅니다.
✓ pass → 여기서 멈춰도 완결입니다. 더 키우려면 2주차에 표가 말한 잠금을 실제 Aurora 에서 잽니다.
✗ fail → 어긋난 작업은 표에서 locks_writes 를 참으로 두고, 까닭을 밝힐 때까지 PR 체크를 붙이지 않습니다.
✓ pass → 어긋난 행이 없거나 그 까닭을 표의 note 에 적었으면 3주차 PR 체크로 넘어갑니다.
마이그레이션 SQL 문장 하나를 머지해도 되는지 정합니다. 네 판정 중 하나와 함께 걸린 나머지 판정, 작업 종류와 알고리즘, 행 수·7일 조회 수·스냅샷 경과 시간을 담아 터미널 표와 PR 코멘트가 그대로 읽습니다.
{"type": "object", "properties": {"verdict_no": {"type": "string"}, "request_no": {"type": "string"}, "pr_no": {"type": "integer"}, "cluster": {"type": "string"}, "table_name": {"type": "string"}, "operations": {"type": "array", "items": {"type": "string"}, "description": "ddl_statement 를 ALTER 절마다 읽어 뽑은 작업 종류입니다. online_ddl_matrix.operation 값이거나, ALTER 가 아닌 문장이면 drop_table·truncate_table 입니다"}, "target_column": {"type": ["string", "null"], "description": "DROP COLUMN 이 지우는 컬럼 이름입니다. DROP COLUMN 이 없으면 null 입니다"}, "algorithm": {"type": ["string", "null"], "enum": ["INSTANT", "INPLACE", "COPY", null], "description": "operations 가 가리키는 online_ddl_matrix 행 중 가장 무거운 값(COPY > INPLACE > INSTANT)입니다. drop_table·truncate_table 이면 null 입니다"}, "locks_writes": {"type": ["boolean", "null"], "description": "operations 가 가리키는 online_ddl_matrix 행 중 하나라도 locks_writes 가 참이면 참입니다. drop_table·truncate_table 이면 null 입니다"}, "row_count": {"type": ["integer", "null"], "minimum": 0, "description": "같은 cluster·table_name 의 table_stats.row_count 입니다"}, "calls_7d": {"type": ["integer", "null"], "minimum": 0, "description": "target_column 과 같은 cluster·table_name·column_name 의 query_digest.calls_7d 합입니다. 그런 다이제스트가 없으면 0, target_column 이 null 이면 null 입니다"}, "snapshot_age_hours": {"type": ["number", "null"], "description": "requested_at 과 같은 cluster 의 cluster_backups.last_snapshot_at 을 UTC 로 옮겨 뺀 시간(시간 단위)입니다. 지우거나 비우는 문장(drop_table·truncate_table·drop_column)에서만 채우고, last_snapshot_at 이 null 이면 null 입니다 last_snapshot_at 이 requested_at 보다 늦으면(PR 을 올린 뒤 찍은 스냅샷) 음수입니다"}, "verdict": {"type": "string", "enum": ["바로 머지 가능", "쓰기 잠금 걸리는 변경", "아직 조회되는 컬럼 삭제", "백업 없이 지우는 변경"]}, "other_hits": {"type": "array", "items": {"type": "string", "enum": ["바로 머지 가능", "쓰기 잠금 걸리는 변경", "아직 조회되는 컬럼 삭제", "백업 없이 지우는 변경"]}, "description": "verdict 로 고르지 않았지만 같은 문장이 함께 걸린 판정입니다"}, "evidence": {"type": "array", "items": {"type": "object", "properties": {"table": {"type": "string", "enum": ["schema_change_requests", "table_stats", "online_ddl_matrix", "query_digest", "cluster_backups"]}, "field": {"type": "string"}, "agg": {"type": ["string", "null"]}, "value": {}}, "required": ["table", "field", "value"]}}, "summary": {"type": "string"}, "hedge": {"type": ["string", "null"]}}, "required": ["verdict_no", "request_no", "pr_no", "cluster", "table_name", "operations", "verdict", "other_hits", "evidence", "summary"]}[
{
"verdict_no": "V-1482-1",
"request_no": "SCR-1482-1",
"pr_no": 1482,
"cluster": "core-aurora",
"table_name": "car_reservations",
"operations": [
"change_column_type"
],
"target_column": null,
"algorithm": "COPY",
"locks_writes": true,
"row_count": 18400000,
"calls_7d": null,
"snapshot_age_hours": null,
"verdict": "쓰기 잠금 걸리는 변경",
"other_hits": [],
"evidence": [
{
"table": "schema_change_requests",
"field": "request_no",
"agg": null,
"value": "SCR-1482-1"
},
{
"table": "schema_change_requests",
"field": "ddl_statement",
"agg": null,
"value": "ALTER TABLE car_reservations MODIFY memo TEXT"
},
{
"table": "online_ddl_matrix",
"field": "algorithm",
"agg": null,
"value": "COPY"
},
{
"table": "online_ddl_matrix",
"field": "locks_writes",
"agg": null,
"value": true
},
{
"table": "table_stats",
"field": "row_count",
"agg": null,
"value": 18400000
}
],
"summary": "memo 컬럼 타입을 바꾸는 MODIFY 는 COPY 로 돌아 복사가 끝날 때까지 1,840만 행 테이블에 쓰기가 멈춥니다.",
"hedge": "memo 가 이미 TEXT 계열이면 실제로는 복사가 일어나지 않을 수 있습니다. 파서는 현재 타입을 모르므로 MODIFY 를 타입 변경으로 읽습니다."
},
{
"verdict_no": "V-1482-2",
"request_no": "SCR-1482-2",
"pr_no": 1482,
"cluster": "core-aurora",
"table_name": "members",
"operations": [
"drop_column"
],
"target_column": "legacy_grade",
"algorithm": "INPLACE",
"locks_writes": false,
"row_count": 4120000,
"calls_7d": 3212,
"snapshot_age_hours": 31,
"verdict": "아직 조회되는 컬럼 삭제",
"other_hits": [
"백업 없이 지우는 변경"
],
"evidence": [
{
"table": "schema_change_requests",
"field": "request_no",
"agg": null,
"value": "SCR-1482-2"
},
{
"table": "query_digest",
"field": "digest_no",
"agg": null,
"value": "QD-01"
},
{
"table": "query_digest",
"field": "column_name",
"agg": null,
"value": "legacy_grade"
},
{
"table": "query_digest",
"field": "calls_7d",
"agg": null,
"value": 3212
},
{
"table": "schema_change_requests",
"field": "requested_at",
"agg": null,
"value": "2026-09-27T10:20:00+09:00"
},
{
"table": "cluster_backups",
"field": "last_snapshot_at",
"agg": null,
"value": "2026-09-25T18:20:00Z"
}
],
"summary": "legacy_grade 를 읽는 쿼리가 최근 7일에 3,212번 돌았고, 이 클러스터의 마지막 수동 스냅샷도 31시간 전입니다.",
"hedge": null
},
{
"verdict_no": "V-1482-3",
"request_no": "SCR-1482-3",
"pr_no": 1482,
"cluster": "core-aurora",
"table_name": "coupon_issue_2023",
"operations": [
"drop_table"
],
"target_column": null,
"algorithm": null,
"locks_writes": null,
"row_count": 9850000,
"calls_7d": null,
"snapshot_age_hours": 31,
"verdict": "백업 없이 지우는 변경",
"other_hits": [],
"evidence": [
{
"table": "schema_change_requests",
"field": "request_no",
"agg": null,
"value": "SCR-1482-3"
},
{
"table": "schema_change_requests",
"field": "ddl_statement",
"agg": null,
"value": "DROP TABLE coupon_issue_2023"
},
{
"table": "schema_change_requests",
"field": "requested_at",
"agg": null,
"value": "2026-09-27T10:20:00+09:00"
},
{
"table": "cluster_backups",
"field": "backup_no",
"agg": null,
"value": "BK-01"
},
{
"table": "cluster_backups",
"field": "last_snapshot_at",
"agg": null,
"value": "2026-09-25T18:20:00Z"
}
],
"summary": "985만 행 테이블을 지우는데 core-aurora 의 마지막 수동 스냅샷이 31시간 전이라 기준 24시간을 넘습니다.",
"hedge": "연속 백업으로 시점 복원은 되지만 테이블 하나를 되살리려면 클러스터를 통째로 새로 띄워야 합니다."
},
{
"verdict_no": "V-1482-4",
"request_no": "SCR-1482-4",
"pr_no": 1482,
"cluster": "core-aurora",
"table_name": "vehicles",
"operations": [
"add_column"
],
"target_column": null,
"algorithm": "INSTANT",
"locks_writes": false,
"row_count": 24310,
"calls_7d": null,
"snapshot_age_hours": null,
"verdict": "바로 머지 가능",
"other_hits": [],
"evidence": [
{
"table": "schema_change_requests",
"field": "request_no",
"agg": null,
"value": "SCR-1482-4"
},
{
"table": "online_ddl_matrix",
"field": "operation",
"agg": null,
"value": "add_column"
},
{
"table": "online_ddl_matrix",
"field": "algorithm",
"agg": null,
"value": "INSTANT"
},
{
"table": "online_ddl_matrix",
"field": "locks_writes",
"agg": null,
"value": false
}
],
"summary": "끝자리에 컬럼을 붙이는 변경이라 INSTANT 로 메타데이터만 바뀌고 쓰기를 막지 않습니다.",
"hedge": null
},
{
"verdict_no": "V-1490-1",
"request_no": "SCR-1490-1",
"pr_no": 1490,
"cluster": "payment-aurora",
"table_name": "payment_logs",
"operations": [
"add_secondary_index"
],
"target_column": null,
"algorithm": "INPLACE",
"locks_writes": false,
"row_count": 52700000,
"calls_7d": null,
"snapshot_age_hours": null,
"verdict": "바로 머지 가능",
"other_hits": [],
"evidence": [
{
"table": "schema_change_requests",
"field": "request_no",
"agg": null,
"value": "SCR-1490-1"
},
{
"table": "schema_change_requests",
"field": "ddl_statement",
"agg": null,
"value": "alter table `socar_pay`.`payment_logs` add index idx_paid_at (paid_at)"
},
{
"table": "online_ddl_matrix",
"field": "operation",
"agg": null,
"value": "add_secondary_index"
},
{
"table": "online_ddl_matrix",
"field": "locks_writes",
"agg": null,
"value": false
},
{
"table": "table_stats",
"field": "row_count",
"agg": null,
"value": 52700000
}
],
"summary": "5,270만 행 테이블이지만 보조 인덱스 추가는 INPLACE 로 도는 동안 쓰기를 허용합니다.",
"hedge": "인덱스를 만드는 동안 쌓인 변경을 끝에 한꺼번에 반영하므로, 쓰기가 몰리는 결제 시간대에는 마지막 단계가 길어질 수 있습니다."
},
{
"verdict_no": "V-1490-3",
"request_no": "SCR-1490-3",
"pr_no": 1490,
"cluster": "payment-aurora",
"table_name": "refund_requests",
"operations": [
"drop_column"
],
"target_column": "old_reason",
"algorithm": "INPLACE",
"locks_writes": false,
"row_count": 310000,
"calls_7d": 0,
"snapshot_age_hours": 24,
"verdict": "바로 머지 가능",
"other_hits": [],
"evidence": [
{
"table": "schema_change_requests",
"field": "request_no",
"agg": null,
"value": "SCR-1490-3"
},
{
"table": "query_digest",
"field": "digest_no",
"agg": null,
"value": "QD-05"
},
{
"table": "query_digest",
"field": "calls_7d",
"agg": null,
"value": 0
},
{
"table": "schema_change_requests",
"field": "requested_at",
"agg": null,
"value": "2026-09-27T14:05:00+09:00"
},
{
"table": "cluster_backups",
"field": "last_snapshot_at",
"agg": null,
"value": "2026-09-26T05:05:00Z"
}
],
"summary": "payment-aurora 에서 old_reason 을 읽은 쿼리가 최근 7일에 없고, 마지막 수동 스냅샷이 정확히 24시간 전이라 기준을 넘지 않습니다.",
"hedge": "core-aurora 의 같은 이름 테이블에는 old_reason 조회가 77번 남아 있지만 다른 클러스터라 이 판정에 넣지 않았습니다."
},
{
"verdict_no": "V-1495-1",
"request_no": "SCR-1495-1",
"pr_no": 1495,
"cluster": "fleet-aurora",
"table_name": "telemetry_staging",
"operations": [
"truncate_table"
],
"target_column": null,
"algorithm": null,
"locks_writes": null,
"row_count": 2300000,
"calls_7d": null,
"snapshot_age_hours": null,
"verdict": "백업 없이 지우는 변경",
"other_hits": [],
"evidence": [
{
"table": "schema_change_requests",
"field": "request_no",
"agg": null,
"value": "SCR-1495-1"
},
{
"table": "schema_change_requests",
"field": "ddl_statement",
"agg": null,
"value": "TRUNCATE TABLE telemetry_staging"
},
{
"table": "cluster_backups",
"field": "backup_no",
"agg": null,
"value": "BK-03"
},
{
"table": "cluster_backups",
"field": "last_snapshot_at",
"agg": null,
"value": null
}
],
"summary": "fleet-aurora 는 수동 스냅샷을 한 번도 찍지 않았는데 230만 행 테이블을 비웁니다.",
"hedge": null
},
{
"verdict_no": "V-1495-2",
"request_no": "SCR-1495-2",
"pr_no": 1495,
"cluster": "fleet-aurora",
"table_name": "charging_sessions",
"operations": [
"add_fulltext_index"
],
"target_column": null,
"algorithm": "INPLACE",
"locks_writes": true,
"row_count": 1000000,
"calls_7d": null,
"snapshot_age_hours": null,
"verdict": "바로 머지 가능",
"other_hits": [],
"evidence": [
{
"table": "schema_change_requests",
"field": "request_no",
"agg": null,
"value": "SCR-1495-2"
},
{
"table": "online_ddl_matrix",
"field": "operation",
"agg": null,
"value": "add_fulltext_index"
},
{
"table": "online_ddl_matrix",
"field": "locks_writes",
"agg": null,
"value": true
},
{
"table": "table_stats",
"field": "stat_no",
"agg": null,
"value": "TS-09"
},
{
"table": "table_stats",
"field": "row_count",
"agg": null,
"value": 1000000
}
],
"summary": "FULLTEXT 인덱스 추가는 쓰기를 막지만 행 수가 정확히 100만이라 '100만을 넘으면' 기준에 걸리지 않습니다.",
"hedge": "TABLE_ROWS 는 추정치라 실제 행 수가 100만을 조금 넘을 수 있습니다. 기준값에 가까운 테이블은 COUNT(*) 로 다시 재 보길 권합니다."
},
{
"verdict_no": "V-1501-2",
"request_no": "SCR-1501-2",
"pr_no": 1501,
"cluster": "core-aurora",
"table_name": "members",
"operations": [
"add_column",
"add_secondary_index"
],
"target_column": null,
"algorithm": "INPLACE",
"locks_writes": false,
"row_count": 4120000,
"calls_7d": null,
"snapshot_age_hours": null,
"verdict": "바로 머지 가능",
"other_hits": [],
"evidence": [
{
"table": "schema_change_requests",
"field": "request_no",
"agg": null,
"value": "SCR-1501-2"
},
{
"table": "schema_change_requests",
"field": "ddl_statement",
"agg": null,
"value": "ALTER TABLE members ADD COLUMN marketing_opt_in TINYINT, ADD INDEX idx_marketing_opt_in (marketing_opt_in)"
},
{
"table": "online_ddl_matrix",
"field": "operation",
"agg": null,
"value": "add_secondary_index"
},
{
"table": "online_ddl_matrix",
"field": "algorithm",
"agg": null,
"value": "INPLACE"
},
{
"table": "online_ddl_matrix",
"field": "locks_writes",
"agg": null,
"value": false
},
{
"table": "table_stats",
"field": "row_count",
"agg": null,
"value": 4120000
}
],
"summary": "컬럼 추가와 보조 인덱스 추가를 한 문장에 묶어 더 무거운 INPLACE 로 돌지만, 두 작업 모두 쓰기를 막지 않아 412만 행 테이블에서도 쓰기가 이어집니다.",
"hedge": "한 문장에 묶으면 컬럼 추가도 INSTANT 로 끝나지 않고 인덱스를 만드는 동안 함께 돕니다. 컬럼만 먼저 넣고 싶으면 두 문장으로 나누는 편이 빠릅니다."
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 네 값 중 하나이고 other_hits 의 값도 모두 네 값 중 하나인지 확인합니다. 2. verdict_no·request_no·pr_no·cluster·table_name·operations·verdict·other_hits·evidence·summary 가 모두 채워졌는지 확인합니다. 3. snapshot_age_hours 가 requested_at 과 last_snapshot_at 을 UTC 로 옮겨 뺀 시간과 같은지 다시 계산합니다. last_snapshot_at 이 null 이면 snapshot_age_hours 도 null 인지 봅니다. 스냅샷이 PR 을 올린 뒤에 찍혔으면 값은 음수이고, 24시간 넘게 앞서지 않았으므로 7번에 걸리지 않습니다. 4. row_count·calls_7d 가 0 이상인지 범위를 봅니다. snapshot_age_hours 는 3번대로 음수일 수 있습니다. 5. operations 중 하나라도 online_ddl_matrix 에서 locks_writes 가 참이고 row_count 가 1,000,000 을 넘으면 verdict 는 쓰기 잠금 걸리는 변경입니다. row_count 가 정확히 1,000,000 인 V-1495-2 는 넘지 않으므로 이 조건에 걸리지 않습니다. 6. operations 에 drop_column 이 있고 calls_7d 가 0 을 넘으면, 5번에 걸리지 않는 한 verdict 는 아직 조회되는 컬럼 삭제입니다. calls_7d 는 같은 cluster 의 다이제스트만 더하므로 V-1490-3 의 0 은 다른 클러스터 호출 77회를 넣지 않은 값입니다. 7. operations 에 drop_table·truncate_table·drop_column 중 하나가 있고 snapshot_age_hours 가 24 를 넘거나 last_snapshot_at 이 null 이면, 5·6번에 걸리지 않는 한 verdict 는 백업 없이 지우는 변경입니다. 스냅샷이 정확히 24시간 전인 V-1490-3 은 이 조건에 걸리지 않습니다. 8. 5·6·7번 중 verdict 로 고르지 않은 조건에 같은 문장이 걸리면 그 판정이 other_hits 에 들어 있는지 봅니다. 9. 5·6·7번에 하나도 걸리지 않으면 verdict 는 바로 머지 가능이고 other_hits 는 빈 배열인지 봅니다. 10. operations 에 drop_table·truncate_table 만 있으면 algorithm 과 locks_writes 는 null 입니다. 그 밖의 값은 online_ddl_matrix.operation 에 실재하는지 대조합니다. 11. operations 가 가리키는 online_ddl_matrix 행에 COPY 가 하나라도 있으면 algorithm 은 COPY 이고, COPY 없이 INPLACE 가 있으면 INPLACE 입니다. 절이 둘 든 V-1501-2 는 add_column(INSTANT)과 add_secondary_index(INPLACE)라 INPLACE 이고, 두 행 모두 locks_writes 가 거짓이라 locks_writes 도 거짓입니다. 12. evidence 의 field 가 그 table 이 가리키는 데이터셋에 실재하는 컬럼인지 대조합니다. 13. agg 가 없는 evidence 의 value 가 그 컬럼에 수록된 행 값과 같은지 확인합니다. 14. summary 가 존댓말로 끝나는지 확인합니다. [평가 러너 · 2주차] 1. 손으로 정답을 단 60문장에 판정을 돌려 판정별 일치율을 냅니다. 2. 시험 클러스터에 sysbench 쓰기 부하를 건 채 표의 작업 12종을 하나씩 돌려, INSERT 가 1초 넘게 멈춘 작업을 세고 online_ddl_matrix.locks_writes 와 다른 행을 표로 냅니다. [수동 검증 · 사람이 봅니다] 1. 쓰기 잠금 걸리는 변경을 언제 어떤 방법으로 적용할지는 Product Engineer 와 Tech Platform 이 정합니다. 2. 아직 조회되는 컬럼을 읽는 코드를 먼저 걷어 낼지, 삭제를 다음 배포로 미룰지는 Product Engineer 가 정합니다.