# 리멤버 1차 면접 2일 압축본

**작성일** 2026-08-10 (D-2)
**면접** 2026-08-12 · Data Engineer 팀장 1명, 팀원 1명
**전제** 일과 병행. 총 학습시간 4시간으로 끝내는 것을 목표로 한다.
**원본** [면접 준비 가이드(전체판)](2026-08-05-remember-data-engineer-interview-prep.md) · [스터디 지도](2026-08-07-remember-interview-study-map.md) · [과제 아키텍처 페이지](../career/remember-interview/assignment.html)

이 문서의 원칙은 하나다. **어려운 말로 정확하게 말하는 것보다, 쉬운 말로 정확하게 말하는 것이 어렵다.** 그래서 모든 항목을 `초등학생 설명 → 정확한 정의 → 내 사례 연결` 3단으로 적었다. 면접장에서는 1단으로 시작해 상대가 더 파면 2단, 3단으로 내려간다.

---

## 0. 시간표 — 4시간 안에 끝내기

| 언제 | 시간 | 무엇 | 왜 |
|---|---|---|---|
| D-2 밤 (8/10) | 90분 | 2장 과제 + 3장 A그룹(과제에 쓴 기술) | 가장 많이 물어볼 곳. 여기만 되면 절반은 넘긴다 |
| D-1 밤 (8/11) | 90분 | 4장 리멤버 스택 + 6장 안 해본 것 스크립트 | 남의 집 지도와 경계선. 외우는 게 아니라 개념만 |
| D-1 밤 | 30분 | 2-2·2-3을 **소리 내어** 5회 | 30초 설명은 입으로 외워야 현장에서 나온다 |
| 당일 아침 | 30분 | 8장 치트시트 1장만 | 숫자와 첫 문장만 다시 본다 |

**하지 말 것**: 전체판 가이드(755줄)를 처음부터 다시 읽는 것. 이미 만든 답변 스크립트는 지금 단계에서 도움이 안 된다. 문장을 외우면 질문이 살짝만 비틀려도 무너진다. 구조와 숫자만 잡는다.

**5장 포트폴리오는 읽지 않는다.** 본인이 직접 한 일이라 이미 알고 있다. 표에 적은 것은 "물어보면 이 각도로 꺼내라"는 앵커 한 줄씩이다. 훑기만 한다.

---

## 1. 팀장이 5년차에게 확인하려는 것 6가지

경력 5년차 데이터 엔지니어 면접에서 실제로 판정되는 건 지식의 양이 아니다. 아래 6개 신호다. 답변마다 **이 중 하나는 반드시 태우고** 지나간다.

| 신호 | 팀장이 속으로 하는 질문 | 내가 태울 재료 |
|---|---|---|
| **① 데이터를 먼저 봤나** | 요구사항만 읽고 코딩부터 시작하는 사람인가 | 구현 전 두 CSV 전수 조사 → `1` vs `1.0`, `전남` vs `전라남도` 210건 발견 |
| **② 되돌릴 수 있게 만들었나** | 사고 나면 이 사람 코드는 복구 가능한가 | RAW 선보존, 컬럼별 변경 이력, 한 트랜잭션 커밋, 멱등 재실행 |
| **③ 모르는 값을 함부로 고치지 않나** | 데이터를 오염시킬 사람인가 | 9자리 사업자번호에 0을 채우지 않고 격리, checksum 불일치 9건은 WARN만 |
| **④ 자기 설계의 한계를 아나** | 자기 코드를 과대평가하는가 | "실행 메타데이터까지 exactly-once는 아니다", "동시 backfill은 지금 안전하다고 말 못 한다" |
| **⑤ 안 해본 걸 안 해봤다고 하나** | 같이 일할 때 믿을 수 있는가 | Spark·Kafka 프로덕션 없음, AWS는 EC2만, 개인정보 운영 없음 |
| **⑥ 혼자 결정했나 팀을 통했나** | 영향 범위 큰 변경을 혼자 지르는가 | 인덱스 224개 삭제 전 95개 쿼리 실행계획 확인, 팀장·CTO 리뷰 |

③④⑤가 5년차와 3년차를 가르는 지점이다. 신입은 "했습니다"를 말하고, 5년차는 "여기까지 했고 여기부터는 안 했습니다"를 말한다.

---

## 2. 과제 — 여기가 면접의 절반

### 2-1. 10초 버전

> 2022년 회사 명단과 2023년 회사 명단을 사업자번호로 맞춰서, 새로 생긴 곳·바뀐 곳·그대로인 곳·이번에 안 보이는 곳을 나누고, 그 판단 근거를 전부 남기는 파이프라인입니다.

### 2-2. 초등학생 버전 (30초) — 이걸 먼저 말한다

> 작년 반 명단이랑 올해 반 명단을 들고 비교한다고 해보세요. 이름·주소·전화번호가 적혀 있고요.
>
> 그냥 눈으로 비교하면 실수가 납니다. **작년에는 "1명"이라고 썼는데 올해는 "1.0명"이라고 썼으면 사람은 같다고 보지만 컴퓨터는 다르다고 봐요.** 주소도 작년엔 "전남", 올해는 "전라남도"라고 쓰면 이사 간 것처럼 보이고요.
>
> 그래서 저는 세 칸으로 나눠서 일했습니다. **첫째 칸에 받은 종이를 원본 그대로 붙여두고, 둘째 칸에서 글씨체를 통일해서 옮겨 적고, 셋째 칸에서 작년 명단이랑 한 줄씩 맞췄습니다.** 나중에 "이거 왜 바뀐 거예요?" 물으면 셋째 칸만 보면 안 되니까, 원본을 첫째 칸에 남겨둔 거예요.
>
> 그리고 **"올해 명단에 없다"고 지우지 않았습니다.** 전학 간 건지, 명단 만드는 사람이 빠뜨린 건지 저는 모르거든요. 그래서 지우는 대신 "올해는 안 보였음"이라고 표시만 해뒀습니다.

세 칸 = RAW / STG / MASTER. 이 비유 하나로 계층 설계·재처리·soft delete가 한 번에 설명된다. **면접에서 용어를 먼저 꺼내지 말고 비유를 먼저 던진 뒤, 상대가 고개를 끄덕이면 "이걸 RAW/STG/MASTER라고 부릅니다"로 붙인다.**

### 2-3. 면접용 버전 (30초)

> 2022 초기 적재와 2023 동기화를 **DAG 하나**로 처리했습니다. 연도만 파라미터로 받아서 같은 코드가 두 회차를 돌립니다.
>
> 입력 파일을 RAW로 원문 보존하고, STG에서 사업자번호·주소·근로자수·날짜를 같은 형식으로 정규화하면서 품질을 판정합니다. 그다음 사업자번호로 조인해서 신규·변경·no-op를 나누고, **마스터 반영 + 변경 이력 + 부재 표시 + 정합성 검사 8종을 한 트랜잭션으로 커밋**합니다. 하나라도 실패하면 이력까지 롤백하고 회차를 FAILED로 마감합니다.
>
> DuckDB 파일에 동시에 쓰지 않도록 태스크를 직렬로 두고 `max_active_runs=1`로 제한했습니다.

### 2-4. 데이터 흐름 (이 그림을 머리에 넣는다)

```mermaid
flowchart LR
  CSV["연도별 CSV<br/>2022 · 2023"] --> RAW
  subgraph RAW["RAW · 원문 보존"]
    R1["raw_workplace<br/>규칙 바뀌면 파일 재수신 없이 여기서 재계산"]
  end
  RAW --> STG
  subgraph STG["STAGING · 정규화 + 품질 판정"]
    S1["stg_workplace<br/>has_valid_key / is_valid 분리"]
    S2["ops_quarantine<br/>격리 · 확인 필요"]
  end
  S1 --> MERGE{"사업자번호 조인<br/>IS DISTINCT FROM"}
  MERGE -->|없던 키| INS["신규 2,174"]
  MERGE -->|5속성 중 1개라도 다름| UPD["변경 4,000"]
  MERGE -->|전부 동일| NOP["no-op 17,087"]
  MERGE -->|소스에 없음| ABS["is_active=false 9,426"]
  INS --> TX
  UPD --> TX
  NOP --> TX
  ABS --> TX
  subgraph TX["단일 트랜잭션 — 전부 성공 or 전부 롤백"]
    M1["master_workplace<br/>사업장당 1행 (과제 결과물)"]
    M2["master_change_log<br/>컬럼별 전·후 값"]
    CHK["정합성 검사 8종"]
  end
  TX --> OPS["ops_sync_run<br/>회차별 입력·정책·건수·검증결과"]
```

**이 그림에서 반드시 말할 3가지**
1. RAW가 앞에 있는 이유 — 규칙이 바뀌어도 원본 파일을 다시 받지 않는다
2. 네모 하나(`TX`)로 묶인 이유 — 절반만 반영된 상태를 남기지 않는다
3. `ops_sync_run`이 밖에 있는 이유 — 여기가 exactly-once가 아닌 지점이다 (2-9 참고)

### 2-5. 실행 구조

```mermaid
flowchart TB
  subgraph IMG["Airflow 이미지 (빌드 시 굽는다)"]
    CODE["DAG · 파이프라인 코드"]
    DATA["입력 CSV (NFC 정규화)"]
  end
  subgraph RUN["docker compose"]
    AF["Airflow scheduler + webserver<br/>max_active_runs=1 · 태스크 직렬"]
    PG[("PostgreSQL<br/>Airflow 메타데이터 전용")]
    VOL[("named volume<br/>DuckDB 작업 파일")]
  end
  IMG --> AF
  AF <--> PG
  AF <--> VOL
  VOL -->|두 회차 SUCCESS 이후에만| OUT["warehouse/employment_insurance.duckdb"]
  UP["./up.sh<br/>검토자는 Docker만 있으면 됨"] --> RUN
```

**왜 바인드 마운트를 안 했나** (물어보면 강한 답): 호스트 경로를 그대로 붙이면 한글 파일명과 Drive 동기화 폴더에서 파일 접근이 실패했다. 실제로 `OSError: Resource deadlock avoided`를 봤고, macOS는 한글을 NFD로 저장하는데 리눅스 컨테이너 안에서는 바이트 단위 조회가 어긋났다. 그래서 입력과 코드를 이미지에 굽고 CSV 이름을 NFC로 정규화했다. **재현성 문제를 실제 실패로 겪고 고쳤다는 점이 이 답변의 값이다.**

### 2-6. 테이블 6개 — 왜 마스터 하나로 안 끝냈나

이 질문은 거의 확실히 나온다. "과제는 마스터 테이블 하나만 요구했는데 왜 6개죠?"

> **마스터만 만들면, Update가 규칙대로 됐는지 확인할 방법이 없습니다.**

| 계층 | 테이블 | 한 줄 역할 | 없으면 못 하는 것 |
|---|---|---|---|
| RAW | `raw_workplace` | 받은 원문 그대로 | 규칙 바꿀 때 파일 재수신 필요 |
| STG | `stg_workplace` | 정규화 + 품질 판정 | 무엇이 왜 제외됐는지 설명 불가 |
| MASTER | `master_workplace` | **사업장당 1행 최신 상태 (과제 결과물)** | — |
| MASTER | `master_change_log` | 컬럼별 전·후 값 | "무엇이 바뀌었나" 답변 불가 |
| OPS | `ops_quarantine` | 격리·확인 필요 기록 | 버린 데이터가 사라짐 |
| OPS | `ops_sync_run` | 회차별 입력·정책·건수·검증결과 | "어느 회차가 이걸 했나" 추적 불가 |

변경 이력은 SCD2 대신 **컬럼 단위 로그**로 갔다. 비교 컬럼이 5개뿐이고 "무엇이 바뀌었나"가 검증의 핵심이었기 때문이다.

### 2-7. 설계 결정 7개 — 각 1줄 + 되받아치는 말

압박 질문이 오면 **먼저 "그 지적 맞습니다"를 인정하고, 왜 그럼에도 이 선택인지**로 간다.

| # | 결정 | 한 줄 근거 | 반론이 오면 |
|---|---|---|---|
| 1 | 데이터 먼저 전수 조사 | `1` vs `1.0`, `전남` vs `전라남도` 210건. 정규화 없었으면 변경 건수가 5천 건 넘게 부풀었다 | "샘플만 봐도 되지 않나요?" → 3만 행이라 전수가 가능했고, 표기 차이는 샘플에 안 걸린다 |
| 2 | **키는 고치지 않는다** | 9자리 사업자번호에 0을 채우면 실재하는 다른 사업장과 충돌한다. 되돌릴 수 없는 오염 | "그럼 그 행은 버리나요?" → 마스터에서만 제외하고 격리 테이블에 남긴다. 버리지 않는다 |
| 3 | 2023 빈 값 → NULL 갱신이 기본 | 과제가 "최신 파일 값으로 갱신"이라고 정의했으니 literal 적용. 보존은 `SYNC_PRESERVE_EXISTING_ON_NULL` 옵션 | "그건 데이터 손실 아닌가요?" → 맞다. 주소 결측 2,249건 중 2,093건은 2022에 값이 있어 추출 누락 가능성이 있다. 그래서 옵션을 만들고 두 정책 모두 검증을 통과시켰다. **운영이면 데이터 소유자와 먼저 합의할 문제** |
| 4 | 2023에 없는 9,426건 삭제 안 함 | 폐업·추출 누락·공개 범위 변경을 구분할 수 없다. 과제 동작도 Insert·Update뿐 | "그건 과제가 요구 안 한 확장 아닌가요?" → 맞다. 해석이 들어간 확장이다. 그래서 부재를 무시하는 옵션도 뒀고, 5개 업무 속성 결과는 그대로 재현된다 |
| 5 | 테이블 6개 | 2-6 표 | — |
| 6 | 한 트랜잭션 | 마스터·이력·부재·검증 8종을 묶고, 하나라도 실패하면 이력까지 롤백 | "성능은요?" → 3만 행 규모라 문제 없었다. 수백만 행이면 청크와 publish pointer로 나눈다 |
| 7 | DuckDB | 연도별 CSV 전체 읽기 + 집합 조인 + 전수 검증이라 컬럼 지향이 맞았다. `read_csv`, `IS DISTINCT FROM` | "SQLite는요?" → 행 단위 OLTP에 맞는 엔진이다. **약 3만 행에서 성능 자랑하려는 선택은 아니었다.** 동시 writer가 필요해지면 중앙 DB로 옮긴다 |

### 2-8. 외울 숫자 — 7개면 충분

덧셈으로 외운다. 개별 숫자를 외우지 말고 **식**을 외운다.

```
30,513 + 2,174 = 32,687     초기 마스터 + 신규 = 최종 마스터
21,087 = 4,000 + 17,087     공통 키 = 변경 + no-op
23,261 + 9,426 = 32,687     활성 + 최근 소스 부재
5,196 = 2,876 + 1,972 + 310 + 30 + 8    주소·근로자수·이름·날짜·업종
8/8, 불일치 0               연도별 정합성 + 2023 최종 대조
```

**함정 질문 대비**: "변경된 사업장이 4,000인데 이력이 5,196이면 계산이 안 맞는데요?"
→ 한 사업장에서 주소와 근로자수가 함께 바뀔 수 있다. **이력을 컬럼별로 한 건씩 남겼기 때문**이다.

품질 판정 결과: 실제 파일 ERROR **0건**, 2023 WARN **29건**, INFO **2,249건**(주소 결측).
테스트: 제출 시점 컨테이너 **107개**, 현재 동일 소스 재검증 **108개** (기록 시점을 나눠 말한다).

### 2-9. 먼저 인정할 한계 3개 — 지적당하기 전에 내가 말한다

이게 신호 ④다. 순서를 바꾸면 방어가 되고, 내가 먼저 말하면 판단력이 된다.

1. **exactly-once는 업무 데이터까지다.** 마스터·이력·부재·검증 8종은 한 트랜잭션이지만, 성공 상태와 건수(`ops_sync_run`)는 그 커밋 뒤에 따로 기록된다. 두 단계 사이에서 죽으면 데이터는 반영됐는데 상태는 RUNNING으로 남을 수 있다. → 운영이면 같은 DB일 때 한 트랜잭션으로 묶고, 저장소가 갈리면 outbox와 오래된 RUNNING 재대조로 복구한다.
2. **동시 backfill은 지금 안전하다고 주장하지 않는다.** `max_active_runs=1` + 직렬 태스크라는 단일 회차 가정에 기대고 있고, 일부 조회가 `run_id` 대신 `source_year` 범위를 쓴다. → 병렬을 허용하려면 모든 접근을 `run_id` 또는 immutable snapshot으로 격리하고 `(run_id, key)` 유일성, lease, publish pointer를 둔다.
3. **메모리에 한 해 분량을 올린다.** `stage()`가 그렇게 동작하므로 수백만 행에서는 청크 처리가 먼저다.

**문서·구현 차이 3건도 먼저 말한다** (이건 진짜 점수가 된다):
- README에 "절단 의심 주소 4건 quarantine"이라고 적었지만 실제로는 수동 프로파일링 후보이고 runtime rule이 없다
- 대표 실행 로그는 동일 core를 쓴 CLI 경로다 (clean extraction에서 Airflow 두 회차도 같은 결과 확인)
- 제출 문서는 107 tests, 현재 재검증은 108 tests

> 말하는 법: "제출 이후에 문서를 실행 산출물과 다시 대조했는데 세 군데 어긋난 부분을 찾았습니다. 먼저 정정하고 시작하겠습니다."

### 2-10. "코드리뷰 해보세요" 대본 — 두 방향 다 준비한다

**(A) 내 과제 코드를 셀프 리뷰하라고 하면** — 잘한 점 자랑이 아니라, 한계를 심각한 순서로 + 조건과 함께. 2-9의 한계 3개가 그대로 재료다.

> "제가 리뷰어라면 세 가지를 지적하겠습니다. 심각한 순서로 —
> ① `stage()`가 한 연도를 메모리에 올립니다. 3만 행이면 20MB라 지금은 문제없지만 수백만 행에서 첫 번째로 무너지는 지점이고, 그때는 정규화를 DuckDB SQL로 내리거나 청크 처리로 바꿔야 합니다.
> ② 성립일자가 과거로 이동한 17건을 WARN으로만 남기고 판정하지 않았습니다. 소급 정정인지 추출 오류인지 데이터만으로 알 수 없어서인데, 운영이라면 이 WARN이 사람에게 도달하는 경로와 원천 정정 요청 절차까지 있어야 '기록'이 '처리'가 됩니다.
> ③ 품질 규칙 12종이 코드에 있어 규칙 변경마다 배포가 필요합니다. 12개 규모에선 리뷰·테스트를 타는 지금이 안전하고, 수십 개로 늘면 선언적 정의로 분리하겠습니다."

닫는 말: "실제로 제출 전 재리뷰에서 테스트 헬퍼가 운영 경로(`apply_master` 한 트랜잭션)와 다른 경로를 검증하던 결함을 찾아 고쳤습니다. 테스트가 통과하는 것보다 무엇을 통과하는지가 중요하다는 걸 다시 배웠습니다."

**(B) 남의 코드를 리뷰하라고 하면** — 평가 대상은 버그 개수가 아니라 접근 순서와 화법이다.

1. **맥락 먼저 (30초):** "목적과 호출 주체가 뭔가요? 가장 걱정되는 부분이 있으신가요? 신규인가요, 수정인가요?" — 바로 코드로 뛰어들지 않는 것 자체가 점수.
2. **읽는 순서를 소리 내어 선언:** "입출력 계약 → 정확성 → 경계 조건 → 실패 시나리오 → 테스트 순으로 보고, 스타일은 마지막에 몰아 말씀드리겠습니다."
3. **파이프라인 코드 체크리스트 6개:** 정확성(유실·중복 경로, NULL을 `=`로 비교하나) → 멱등성(두 번 돌리면 중복 적재되나) → 트랜잭션 경계(중간에 죽으면 절반 반영이 남나) → 경계값(빈 파일·0건·중복 키) → 조용한 실패(예외 삼키기, 실패해도 0 반환) → 테스트(운영 경로를 타나).
4. **화법 = 심각도 라벨 + 질문형:** blocker "이 UPDATE에 WHERE가 없는데 의도된 건가요?" / major "재실행하면 중복 적재될 것 같은데 UPSERT가 앞에 있어야 하지 않을까요?" / nit "취향 차이일 수 있는데 매직 넘버는 상수로." 좋은 점 하나는 꼭 언급.
5. **요약으로 마감:** "blocker 1, major 1, 나머지 nit — blocker만 고치면 머지해도 된다고 생각합니다."

하지 말 것: 스타일부터 지적, "다 다시 짜야 해요", 확신 없는 단정. A→B로 이어지면 "체크리스트 6개는 제 과제의 검증 8종과 같은 관점입니다"로 연결한다.

---

## 3. 기술 용어 — 초등학생 설명 카드

각 카드는 **`쉬운 말` / `정확히` / `내 연결`** 3줄이다. 면접에서는 1줄로 시작한다.

### A그룹 — 과제에서 직접 쓴 것 (여기는 확실히 알아야 함)

**멱등성 (idempotency)**
- 쉬운 말: 같은 버튼을 열 번 눌러도 결과가 한 번 누른 것과 같은 성질. 엘리베이터 버튼을 여러 번 눌러도 엘리베이터는 한 번만 온다.
- 정확히: 같은 입력으로 재실행해도 상태가 같은 값으로 수렴한다.
- 내 연결: 같은 2023 스냅샷을 다시 넣으면 **신규 0, 속성 Update 0, 변경 이력 증가 0**. 단, RAW·STG 교체와 상태 컬럼 UPDATE, 실행 기록은 다시 쓴다. 그래서 **"아무것도 안 쓴다"는 과장이고, 정확한 표현은 "업무 결과가 안 변한다"**다.

**트랜잭션**
- 쉬운 말: 계좌이체. 내 통장에서 빠지고 상대 통장에 들어가는 게 **둘 다 되거나 둘 다 안 되거나** 둘 중 하나여야 한다. 하나만 되면 사고다.
- 정확히: 여러 작업을 하나의 원자 단위로 묶어 전부 성공 또는 전부 롤백.
- 내 연결: 마스터 반영 + 변경 이력 + 부재 표시 + 검증 8종을 한 트랜잭션. 검증이 실패하면 이력까지 되돌리고 회차를 FAILED로 마감.

**RAW / STG / MASTER**
- 쉬운 말: 영수증 원본 / 가계부에 옮겨 적은 것 / 이번 달 최종 정산표. 정산이 틀리면 영수증을 다시 보면 된다.
- 정확히: 원문 보존 계층 / 정규화·품질판정 계층 / 소비자가 쓰는 최신 상태 계층.
- 내 연결: 정규화 규칙이 바뀌어도 공단에 파일을 다시 요청하지 않고 RAW에서 재계산한다.

**`IS DISTINCT FROM`**
- 쉬운 말: 보통 SQL에서 "빈칸"과 "빈칸"을 비교하면 컴퓨터가 "몰라"라고 답한다. 이건 "둘 다 빈칸이면 같은 거야"라고 제대로 답해주는 비교 방식이다.
- 정확히: NULL-safe 비교. `NULL = NULL`은 UNKNOWN이지만 `NULL IS NOT DISTINCT FROM NULL`은 TRUE.
- 내 연결: 값→NULL, NULL→값 변경을 놓치지 않으려고 5개 속성 비교에 사용. hash 비교 대신 직접 비교를 고른 이유는 **어느 컬럼이 바뀌었는지 이력을 남겨야 했고**, Python과 SQL 사이 NULL·문자열 직렬화 규칙 차이로 hash가 어긋날 위험을 피하려는 것.

**soft delete**
- 쉬운 말: 지우개로 지우지 않고 옆에 연필로 "오늘은 안 보임"이라고 적어두는 것.
- 정확히: 물리 삭제 대신 상태 플래그로 비활성 표시.
- 내 연결: `is_active=false`, `absent_since_year=2023`. 내가 확인한 사실은 "최근 소스에 없었다"까지다. 폐업이라고 단정하지 않았다.

**backfill / catchup / `max_active_runs`**
- 쉬운 말: 밀린 일기를 몰아 쓰는 것(backfill). 몰아 쓰되 **한 번에 한 장씩만** 쓰게 막아둔 게 `max_active_runs=1`.
- 정확히: 과거 구간 재실행 / 스케줄러가 미실행 구간을 자동 생성 / 동시 실행 DAG run 수 상한.
- 내 연결: `@yearly` + `catchup=true`로 2022·2023이 자동 생성된다. 다만 `max_active_runs=1`은 동시 실행만 막지 **연도 순서를 보장하지 않아서**, 2023 뒤 2022가 들어오는 역행은 merge 전에 별도 가드로 막았다.

**데이터 품질(DQ)과 차단선**
- 쉬운 말: 시험지 채점할 때 "이건 0점 처리", "이건 빨간 줄만 긋고 넘어감"을 미리 정해두는 것.
- 정확히: ERROR는 반영 차단, WARN은 기록 후 진행, INFO는 관측만.
- 내 연결: 키 결측·형식 오류·근로자수 파싱 오류·중복 탈락은 ERROR로 제외. checksum·날짜 이상은 외부 정답이 없어 고치지 않고 WARN. 주소 결측은 INFO. **`has_valid_key`를 더 넓은 `is_valid`와 분리**해서, 속성이 ERROR여도 업무 키가 유효하면 "현재 소스에서 본 키"로 계산해 잘못된 부재 판정을 막았다.
- 검증 8종: 키 유일성 / 행 수 증분 / 분류 합계 / 소스 키 포함 여부 / 정책값 일치 / NULL 보호 / 활성 상태 / 미래 `last_seen_year` 존재 여부.

**reconciliation (대조)**
- 쉬운 말: 이사하고 나서 짐 개수를 다시 세는 것.
- 정확히: 이관·재처리 후 원본과 결과를 다시 맞춰 누락·차이를 찾는 절차.
- 내 연결: 2023 STG와 활성 마스터 직접 대조 **불일치 0건**. IDC 이관에서도 전후 테이블 행 수를 대조했다.

### B그룹 — 리멤버 스택 (개념만. 운영 경험처럼 말하지 않는다)

**CDC (Change Data Capture)**
- 쉬운 말: 책 전체를 다시 베끼는 대신, **어제 이후 고친 부분만** 옮겨 적는 것.
- 정확히: 원본 DB의 변경 로그(MySQL이면 binlog)를 읽어 변경분만 전파.
- 내 연결: **내가 운영한 CDC는 약 1시간 단위로 binlog 파일을 전달하는 배치형 동기화다.** Kafka 기반 실시간 스트리밍과는 처리 방식도 지연 기준도 다르다. 이 문장을 먼저 말한다.

**Kafka / MSK**
- 쉬운 말: 여러 사람이 넣고 여러 사람이 꺼내 쓰는 **줄 서는 컨베이어 벨트**. 꺼내 간 사람마다 "나는 몇 번째까지 가져갔다"는 표시를 따로 들고 있다.
- 정확히: topic을 partition으로 나눠 순서를 보장하고, consumer group이 offset을 관리하는 분산 로그.
- 내 연결: **프로덕션 운영 경험 없음.** topic·partition·consumer group의 역할까지만 정확히 설명한다.

**Spark**
- 쉬운 말: 한 사람이 다 계산하기엔 너무 큰 숙제를, 반 전체에 나눠주고 걷어서 합치는 방식. 나눠주는 사람이 Driver, 푸는 사람들이 Executor.
- 정확히: Driver가 DAG를 짜고 Executor가 파티션 단위로 분산 실행.
- 내 연결: **프로덕션 운영 경험 없음.** "단일 노드에서 메모리·I/O·처리시간 한계를 먼저 측정하고, 분산 비용보다 이익이 커질 때 검토한다"가 내 답.

**Iceberg / S3 Tables**
- 쉬운 말: 파일 창고에 물건을 쌓아두고, **"지금 이 순간 창고에 뭐가 들어있는지" 목록표를 따로 관리**하는 방식. 목록표를 바꾸면 창고를 안 건드려도 테이블 내용이 바뀐다.
- 정확히: 실제 데이터는 Parquet 파일이고 Iceberg metadata(snapshot → manifest → data file)가 현재 테이블 구성을 정의한다. S3 Tables는 그걸 AWS가 관리형으로 제공.
- 내 연결: 개념 학습만. 다만 **"파일을 테이블처럼 쓰면 운영 작업이 생긴다"**는 지점은 내 문제의식과 붙는다.

**compaction / snapshot 만료 / orphan file**
- 쉬운 말: 작은 상자가 너무 많아지면 꺼낼 때마다 상자를 다 열어봐야 하니까 **큰 상자로 합치는 것**(compaction). 옛날 목록표를 언제까지 보관할지 정하는 게 snapshot 만료. 목록표에서 빠졌는데 창고에 남아 있는 물건 치우는 게 orphan file 정리.
- 정확히: CDC가 작은 커밋을 자주 하면 small file·snapshot·미참조 파일이 누적되므로 유지보수가 운영의 일부가 된다.
- 내 연결: 리멤버 공개 사례는 목표 파일 크기를 **512MB**로 잡았다. 보편 정답은 아니고 해당 워크로드의 선택이라는 단서를 같이 붙인다.

**StarRocks (OLAP)**
- 쉬운 말: 창고(S3)에서 매번 꺼내 세면 느리니까, **자주 보는 것만 앞쪽 진열대에 미리 정리**해두는 것.
- 정확히: 레이크하우스 데이터를 저지연으로 서빙하는 MPP 분석 엔진.
- 내 연결: **프로덕션 경험 없음.** 전환 판단 기준만 말한다 — 소비 쿼리, 동시 사용자, freshness, 비용. "분석용 대량 조회가 운영 트랜잭션을 방해하거나 집계 응답시간을 운영 DB에서 못 맞출 때" 검토한다.

**Lake Formation**
- 쉬운 말: 도서관 출입증(IAM)과 **"이 책의 이 페이지만 볼 수 있음"** 권한(Lake Formation)은 다른 것이다.
- 정확히: IAM은 AWS API 호출 가능 여부, Lake Formation은 DB·테이블·컬럼·행 단위 데이터 권한. 둘 다 있어야 접근된다.
- 내 연결: 개념 학습만. 개인정보 다루는 팀이면 **재처리 산출물에도 같은 정책이 걸려야 한다**는 게 내가 주목한 지점.

**Glue PySpark Full Load**
- 쉬운 말: 이사할 때 **원래 집에서 직접 짐을 빼면 살고 있는 사람이 불편하니까, 똑같은 집(복제본)을 하나 만들어서 거기서 뺀다.**
- 정확히: Full Load 전용 Aurora replica를 만들고 Glue PySpark가 JDBC로 병렬 스캔해 S3 Tables에 적재.
- 내 연결: 대량 이관의 첫 목표는 속도보다 **영향 격리**라는 원칙. 내 IDC 이관에서도 같은 순서였다.

### C그룹 — 내 포트폴리오 용어 (물어보면 파고들 곳)

**collation**
- 쉬운 말: "APPLE"과 "apple"을 같은 단어로 볼지 다른 단어로 볼지 정하는 규칙. **두 테이블이 이 규칙을 다르게 갖고 있으면 서로 대조할 때 인덱스를 못 쓴다.**
- 내 연결: 뷰를 통한 조인이 인덱스를 못 타고 **약 130만 건 전체 스캔**, 테이블 직접 조인은 인덱스를 타고 **약 6,000건**. 실행계획으로 확인했다. **단위 주의 — 이건 실행계획상 예상 스캔 행 수지 응답시간이나 CPU 감소율이 아니다.**

**실행계획 (EXPLAIN)**
- 쉬운 말: DB에게 "너 이 질문에 답하려고 **어떤 순서로 몇 개나 뒤져볼 거야?**"를 미리 물어보는 것.
- 내 연결: 인덱스 311개 중 224개를 지울 때 "아마 안 쓸 것"이 아니라, 분석 엔진이 쓰는 95개 쿼리의 실행계획을 전부 확인해서 근거를 만들었다.

**backpressure / `429 + Retry-After`**
- 쉬운 말: 식당이 꽉 찼는데 손님을 계속 안으로 들이면 안에서 사고가 난다. **"지금은 자리 없어요, 20분 뒤에 오세요"라고 밖에서 말해주는 것**이 backpressure.
- 정확히: 처리 용량 초과 시 요청을 무한히 쌓지 않고 호출자에게 거절과 재시도 시점을 돌려준다.
- 내 연결: 80개 넘는 크롤러의 DB 쓰기를 HTTP Broker 하나로 모으고 bounded queue를 뒀다. 큐가 차면 429 + Retry-After. **큐가 찼는데 계속 받아서 메모리에 쌓으면 장애가 더 커진다**가 핵심 문장.

**OLTP vs OLAP**
- 쉬운 말: 편의점 계산대(한 사람 한 번씩 빠르게)와 월말 매출 집계(전체를 한 번에 훑기). 잘하는 일이 다르다.
- 내 연결: 과제에서 DuckDB를 고른 이유이자, StarRocks 전환 판단 기준의 뼈대.

---

## 4. 리멤버 스택 — 남의 집 지도

> **말하기 전 안전 문구**: "공개된 AWS 기술 블로그와 채용 공고에서 본 구조입니다. 내부 규칙이나 임계값은 모르고, 제가 운영해 본 스택도 아닙니다."

이 경계를 먼저 긋고 들어가면 그다음 설명은 전부 가산점이 된다. 안 긋고 아는 척하면 한 번에 무너진다.

```mermaid
flowchart LR
  subgraph OLTP["운영 (OLTP)"]
    AUR[("Aurora MySQL<br/>writer")]
    REP[("Full Load 전용<br/>replica")]
  end
  AUR -->|binlog| DBZ["Debezium<br/>변경 이벤트 추출"]
  DBZ --> MSK["Amazon MSK (Kafka)<br/>topic · partition"]
  AUR -.복제.-> REP
  REP -->|"JDBC 병렬 스캔"| GLUE["AWS Glue PySpark<br/>Full Load"]
  MSK -->|"PK upsert"| S3T
  GLUE -->|"초기 적재"| S3T
  subgraph LAKE["레이크하우스"]
    S3T[("S3 Tables · Iceberg<br/>Parquet + metadata")]
    MAINT["compaction · snapshot 만료<br/>orphan file 정리"]
    OBS["PyIceberg 자체 수집기<br/>file count · size · partition"]
  end
  S3T --- MAINT
  S3T --- OBS
  S3T --> SR["StarRocks on EKS<br/>저지연 OLAP"]
  S3T --> ATH["Athena · BI"]
  SR --> APP["대시보드 · API"]
  LF["Lake Formation<br/>DB·table·column·row 권한"] -.권한 판정.-> S3T
  LF -.-> ATH
```

**이 그림에서 내가 꺼낼 수 있는 질문 3개** (역질문으로 써도 좋다):
1. Full Load와 CDC의 접속점을 어떻게 잡으셨나요? (아래 그림)
2. S3 Tables의 관리형 maintenance 실행 여부를 무엇으로 확인하시나요? (PyIceberg 관측)
3. StarRocks와 Athena의 책임 경계는 freshness 기준으로 나누시나요?

### Full Load ↔ CDC 접속점 — 가장 물어보기 좋은 지점

```mermaid
sequenceDiagram
  participant DB as Aurora replica
  participant FL as Glue Full Load
  participant CDC as Debezium/Kafka
  participant T as S3 Tables
  Note over DB,CDC: ① 기준점 먼저 고정 (binlog position / GTID)
  DB->>CDC: 이 위치부터 이벤트 수집 시작
  DB->>FL: 스냅샷 읽기 (기준점 시각 기준)
  FL->>T: 초기 전량 적재
  Note over CDC: 그동안 발생한 변경은 버퍼에 쌓임
  CDC->>T: ② 기준점 이후 이벤트를 PK upsert로 재적용
  Note over T: ③ 키 · 건수 · 핵심 속성 대조
```

**왜 이게 어려운가 (초등학생 버전)**: 책을 통째로 베끼는 동안에도 원본은 계속 고쳐진다. 그래서 **"몇 시 몇 분까지의 내용을 베낀 건지" 시각을 먼저 못 박고**, 그 이후 고쳐진 부분만 따로 모았다가 나중에 덧칠한다. 덧칠할 때는 같은 줄을 두 번 덧칠해도 결과가 같도록 **"줄 번호(PK) 기준으로 덮어쓰기"**를 쓴다.

**내 과제와의 연결**: 여기서 PK upsert가 하는 역할이 내 과제의 `IS DISTINCT FROM` 비교 + 사업자번호 기준 merge와 같은 문제다. **재적용해도 결과가 같아야 한다**는 요구가 동일하다. 이 연결을 말할 수 있으면 "과제만 한 사람"에서 "구조를 이해한 사람"으로 올라간다.

---

## 5. 포트폴리오 8개 — 앵커 한 줄씩 (읽기만, 외우지 않음)

본인이 한 일이므로 내용은 이미 안다. **각 항목에서 "면접에서 꺼낼 각도" 하나씩만** 정리했다.

| # | 항목 | 꺼낼 각도 한 줄 | 숫자 1개 |
|---|---|---|---|
| 01 | OS 패키지 취약점 수집 정확도 | CVE ID만 맞아서는 부족하다. 어느 버전에서 고쳐졌는지까지 맞아야 스캐너가 신뢰된다 | 12종 배포판 × 4가지 제공 방식(API·OVAL·Tracker·Git) |
| 02 | 라이브러리 컴포넌트 테이블 재설계 | CPU 부하를 보고 서버를 키우는 대신 슬로우 쿼리→실행계획으로 내려가니 원인이 collation·식별자 정책이었다 | 실행계획 예상 스캔 행 **130만 → 6,000** |
| 03 | 인덱스 최적화 & 용량 절감 | 지워도 되는 **근거를 먼저 만들고** 지웠다 (95개 쿼리 실행계획 전수 확인) | 인덱스 **311개 중 224개** 삭제 · 9.6TB→4TB · 고객사 DB 생성 12h→6h |
| 04 | RAW 보존·재파싱 ETL | 외부에서 다시 받아야만 재처리되던 구조를 원본 선보존으로 바꿨다 → **과제 설계의 원형** | 적용 소스의 재처리 외부 요청 **0건** (`2일→5분`은 설계 산정 예시, 실측 아님) |
| 05 | K8s·Airflow 운영 | 노드 증설과 pool·backoff·resource request를 **함께** 조정했다. 최초 클러스터 설계자라고 말하지 않는다 | Pod 할당 실패 주 70~100건 → **주 1~5건** |
| 06 | DML Broker | 80개 크롤러가 각자 재시도를 관리하던 걸 공통 운영 규칙 하나로 모았다. 각 담당자가 자기 크롤러를 이관 | lock·connection 오류 하루 3~4건 → 적용 후 **4개월 0건** |
| 07 | AWS→IDC 이전 | 장비가 없어서 먼저 **수집 시간 분산**으로 버티고, 준비된 뒤 DB 역할을 분리했다 (임시복구→구조개선 순서) | 월 10~20건 → 5건 미만 → 전환 후 **1~2건** |
| 08 | Grafana 모니터링 | 서버 자원·DB·수집 누락·고객사 동기화를 **같은 시간축**에서 봤다 | 10~20대 서버 (탐지시간 감소율은 미확인) |

**추가 카드**: MySQL 8.4 최대 약 30억 행 사전 검증에서 HDD 쓰기 회귀를 보고 `innodb_flush_method` 기본값 변화로 원인을 좁혔다. 하드웨어별 기준을 나누고 단계적으로 적용했다.

**단위 방어 3개** (과장으로 오해받기 쉬운 지점):
- `130만 → 6,000`은 **실행계획상 예상 스캔 행 수**다. 응답시간·CPU 감소율은 기록으로 확인되지 않았다.
- `2일 → 5분`은 **설계 산정 예시**다. 실측 성과로 말하지 않는다.
- IDC 이관에서 확인된 것은 **전후 테이블 행 수 대조**까지다. 명시적 rollback 실행 여부는 기록으로 확인되지 않는다.

---

## 6. 안 해본 것 스크립트 — 5년차의 가장 강한 무기

각 항목은 **`없다` → `대신 있는 것` → `어떻게 채울지`** 3박자. 미안해하지 않고 담백하게.

| 질문 | 없다 | 대신 있는 것 | 채우는 법 |
|---|---|---|---|
| Spark·Kafka | 프로덕션 운영 경험 없음 | Python·SQL·Airflow·Kubernetes·MySQL로 수십억 행 DB와 80개 크롤러의 스케줄링·pool/backoff·재처리·동시성·backpressure | 팀의 실제 데이터량과 지연 요구 기준으로 실행 모델부터 |
| OLAP DB (StarRocks·ClickHouse) | 프로덕션 없음 | 과제의 DuckDB 전수 검증, 실무의 MySQL 실행계획과 대규모 배치 | 소비 쿼리·동시 사용자·freshness·비용 확인 후 판단 |
| **AWS** | RDS·S3·EFS 운영 경험 없음. **제출 자료의 관련 태그는 최종 검수에서 놓친 잘못된 표기** | EC2에서 self-managed MySQL과 애플리케이션 운영. 온프레미스 SeaweedFS 설계·단계적 이관 | 질문받으면 바로 정정. SeaweedFS 경험을 S3 운영이라고 말하지 않는다 |
| 개인정보 데이터 운영 | 리멤버 수준의 개인정보 운영 경험 없음 (내 경력은 보안 메타데이터 중심) | 최소 권한·마스킹·보존/삭제·재처리 산출물 동일 정책·접근 로그라는 설계 원리 | 수집 목적·접근 주체·보존 삭제 규칙·감사 경계를 먼저 확인 |
| MDM 전용 조직 운영 | 없음 | 식별자 정책 재설계, 원천 레지스트리 전수 대조, RAW·이력·재처리 구조 | 자동 연결 / 사람 검토 / 보류로 나누고 false merge·false split 비율을 표본 검사 |

**AWS 태그 오기는 면접 초반에 내가 먼저 꺼낸다.** 지적당하면 실수지만, 먼저 말하면 검수 태도가 된다.

**답변 만들면 안 되는 3개** — 확인된 사건이 없으므로 그럴듯하게 지어내지 않는다:
- 대인 갈등 사례 (상대 우려·내 양보·최종 결정이 확인된 사건 없음)
- 직장에서의 개인 실패 (실제 손실과 본인 책임이 함께 확인된 사건 없음)
- 받은 피드백으로 행동이 바뀐 사례

실패 경험을 물으면: **"직장 실패 대신 최근 제출 과제에서 사전 발견한 near-miss로 답해도 될까요?"**로 먼저 묻는다. → 독립 재리뷰에서 README 필수 항목 누락, production에서 안 쓰는 경로를 검증한 테스트, 재패키징 후 실행 권한 문제를 발견했다. 제출물 소유자의 검수 실패로 받아들이고 README 복구, production-path 테스트, 파일 모드·인코딩 검사, 한글 경로 clean extraction E2E를 추가했다. 제출 전 Airflow 두 회차 성공과 source-master 불일치 0을 확인했다. (AI 도구도 썼지만, 지적 내용을 요구사항과 실행 결과로 확인하고 제출을 결정한 책임은 나에게 있다.)

---

## 7. 역질문 3개 — 하나만 골라도 됨

과제 얘기가 끝날 때쯤 던지면 대화가 이어진다. 회사 복지 말고 **팀의 실제 판단 기준**을 묻는다.

1. **"Full Load와 CDC의 접속점을 어떻게 잡으셨는지 궁금합니다.** 공개 자료로는 전용 replica를 쓰신 것까지 봤는데, 기준점 이후 이벤트 재적용에서 어려웠던 부분이 있으셨을까요?"
2. **"식별·매핑에서 자동 연결과 사람 검토를 나누는 기준이 있으신가요?** false merge와 false split 중 어느 쪽 비용을 더 크게 보시는지 궁금합니다."
3. **"데이터 품질 규칙 중 파이프라인을 멈추는 것과 경고만 남기는 것을 나누는 기준이 팀에 정해져 있나요?"**

---

## 8. 당일 아침 30분 치트시트

이 페이지만 보고 들어간다.

**첫 90초**
> 여러 곳에서 다른 이름으로 들어오는 데이터를 '결국 같은 대상인지' 판단해 연결해 온 데이터 엔지니어입니다. 5년간 오픈소스 제품과 버전, 취약점, 12종 Linux 배포판의 보안 정보를 맞춰 왔습니다. 잘못 연결되거나 빠진 데이터는 식별 규칙부터 저장 구조와 재처리 방법까지 고쳤습니다. 리멤버에서는 명함·프로필·회사 데이터를 오래 믿을 수 있게 만들고 싶습니다.

**과제 30초 (2-3 그대로)**
RAW 보존 → STG 정규화·품질판정 → 사업자번호 조인으로 신규/변경/no-op → 마스터·이력·부재·검증 8종을 한 트랜잭션 → DuckDB 단일 writer라 직렬 + `max_active_runs=1`.

**숫자**
```
30,513 + 2,174 = 32,687
21,087 = 4,000 + 17,087
23,261 + 9,426 = 32,687
5,196 = 2,876 + 1,972 + 310 + 30 + 8
8/8 · 불일치 0 · ERROR 0 · 테스트 107(제출)/108(현재)
```

**먼저 말할 정정 2개**
- 제출 자료의 AWS 태그(RDS·S3·EFS)는 검수에서 놓친 오기. 실제 운영은 EC2 self-managed MySQL.
- 문서·구현 차이 3건 (절단 주소 4건 quarantine 표기 / 대표 로그가 CLI 경로 / 107 vs 108 tests).

**먼저 인정할 한계 3개**
exactly-once는 업무 데이터까지(실행 기록은 별도 커밋) · 동시 backfill 미보장 · `stage()` 메모리 적재.

**답변 길이 규칙**
`상황 1문장 → 선택 2문장 → 검증 1문장 → 한계 1문장`. 길어지면 여기로 되돌아온다.

**말하기 순서 규칙**
비유 먼저 → 용어는 나중. 상대가 끄덕이면 그때 `RAW/STG/MASTER`, `멱등성`, `IS DISTINCT FROM`을 붙인다.

**마지막 한 줄**
확인한 것과 확인 못 한 것을 같은 문장 안에서 구분하면, 나머지 답변의 신뢰도가 같이 올라간다.

---

**출처** `ai/repo-notes/remember-data-engineer-assignment-claude.md` · `human/reports/2026-08-05-remember-data-engineer-interview-prep.md` · `human/reports/2026-08-07-remember-interview-study-map.md` · `human/study/content/remember-data-engineering/01~07` · `human/career/remember-interview/assignment.html` · 기준일 2026-08-10
