공부

직무 면접 예상 질문 리스트 - 이력서 관련

munsik22 2025. 8. 25. 21:55

KlickLab 프로젝트

Q1. KlickLab 프로젝트에서 API 응답 속도를 10.67초에서 60ms로 개선했다고 하셨습니다. 어떤 지표를 보고 병목 지점을 어떻게 특정하셨나요? 또한, 집계 테이블과 MV(Materialized View) 외에 고려했던 다른 해결 방안이 있었다면 무엇인가요?

  • (병목 지점 특정): 대시보드에서 여러 필터(GROUP BY, JOIN, WHERE)가 복합적으로 적용된 쿼리를 실행할 때 응답 시간이 10초 이상 걸리는 것을 직접 확인하며 병목을 특정했습니다. 특히 1,000만 건 이상의 원본 테이블을 직접 조회하는 것이 원인이라고 판단했습니다.
  • (다른 해결 방안): 당시 가장 큰 문제는 원본 데이터를 직접 가공하는 비용이었습니다. 그래서 쿼리 튜닝 같은 단편적인 해결책보다는, 자주 사용하는 지표를 미리 가공해두는 집계 테이블과 Materialized View 방식이 가장 근본적인 해결책이라 판단하여 집중했습니다.

  • 문제 진단 및 병목 특정: 가장 먼저 확인한 지표는 API 응답 시간이었습니다. 특히 사용자가 대시보드에서 기간, 유입 경로 등 여러 필터를 복합적으로 적용했을 때 응답 시간이 최대 10.67초까지 급증하며 타임아웃이 발생하는 현상을 발견했습니다. 백엔드 로그와 ClickHouse 쿼리 로그를 분석한 결과, 1,000만 건이 넘는 원본 events 테이블에 대해 GROUP BY, JOIN, WHERE 조건이 복잡하게 걸리는 분석 쿼리가 직접 실행되는 것이 병목의 핵심 원인임을 특정할 수 있었습니다. 매번 요청 시마다 거대한 원본 데이터를 가공하는 비용이 너무 컸던 것입니다.
  • 고려했던 다른 방안: 초기에는 Redis 같은 인메모리 DB에 조회 결과를 캐싱하는 방안도 잠시 고려했습니다. 하지만 사용자가 설정하는 필터 조건이 너무 다양해서 모든 경우의 수를 캐싱하기에는 비효율적이라고 판단했습니다. 지금의 집계 테이블 방식이 어떤 필터 조합에도 안정적인 성능을 제공할 수 있는 더 근본적인 해결책이라고 확신했습니다.

Q2. ClickHouse의 ReplacingMergeTree와 AggregatingMergeTree 엔진을 사용하셨는데, 두 엔진의 차이점은 무엇이며 각각 어떤 기준으로 선택하셨는지 설명해주세요.

  • (ClickHouse 엔진 선택): 테이블의 특성에 맞춰 엔진을 선택했습니다. 예를 들어, 최종 집계 데이터처럼 중복 없는 유일한 값을 보장해야 하는 테이블에는 ReplacingMergeTree를, 특정 키 기준으로 데이터를 계속 합산해야 하는 통계 테이블에는 AggregatingMergeTree를 적용하여 데이터의 정합성과 쿼리 효율을 높였습니다.

  • 두 엔진은 데이터를 처리하는 방식에 뚜렷한 차이가 있어, 테이블의 목적과 특성에 맞게 선택하여 적용했습니다.
    • AggregatingMergeTree: 이 엔진은 이름 그대로 데이터를 집계(Aggregate)하는 데 특화되어 있습니다. 동일한 정렬 키를 가진 데이터가 들어오면 기존 데이터와 새로 들어온 데이터를 병합하여 합산이나 평균 같은 집계 함수 상태를 저장합니다. 저는 대시보드의 통계 지표(예: 일일 방문자 수, 페이지뷰 합계)를 미리 계산해두는 agg_ 테이블에 이 엔진을 적용했습니다. 이를 통해 쿼리 시점이 아닌 데이터 적재 시점에 미리 연산을 수행하여 조회 속도를 크게 향상시킬 수 있었습니다.
    • ReplacingMergeTree: 이 엔진은 데이터의 중복을 제거하고 항상 최신 버전의 데이터만 남기는 데 사용됩니다. 정렬 키를 기준으로 중복된 데이터가 들어오면 기존 데이터를 새로 들어온 데이터로 대체합니다. 이 특성은 이벤트 데이터의 최종 상태를 저장하거나, 중복 수집될 수 있는 데이터를 정제해야 하는 flat_ 테이블에 적합하다고 판단하여 적용했습니다.
  • 결론적으로, 지속적인 통계 집계가 필요한 테이블에는 AggregatingMergeTree를, 데이터의 유일성 또는 최신 상태 유지가 중요한 테이블에는 ReplacingMergeTree를 사용하여 데이터의 정합성을 확보하고 쿼리 최적화와 TTL(데이터 유효기간) 관리까지 고려한 설계를 진행했습니다.

Q3. Node.js의 GC(Garbage Collection)로 인한 RPS 급락 문제를 수평 확장으로 해결하셨습니다. 'Stop-the-World'가 무엇인지, 그리고 수평 확장이 이 문제의 근본적인 해결책이 될 수 있었던 이유를 설명해주세요.

  • ('Stop-the-World' 와 수평 확장): 'Stop-the-World'는 Node.js의 가비지 컬렉션(GC)이 실행되는 동안 자바스크립트 실행이 완전히 멈추는 현상입니다. 이 때문에 서버 요청 처리가 주기적으로 중단되어 RPS 급락이 발생했습니다. 수평 확장은 각 서버 인스턴스가 독립적인 프로세스로 동작하므로, 한 인스턴스에서 GC가 발생해도 다른 수십 개의 인스턴스는 정상적으로 요청을 처리할 수 있습니다. 즉, GC의 영향을 전체 서비스가 아닌 개별 인스턴스 수준으로 분산시켜 문제의 파급력을 없앤 것입니다.

  • 'Stop-the-World'는 이름 그대로 세상이 멈추는 현상입니다. Node.js는 싱글 스레드 기반인데, 메모리 정리를 위한 Garbage Collection(GC)이 실행되는 동안에는 이 메인 스레드가 완전히 멈추게 됩니다. 저희 로그 수집 서버는 17대의 고사양 인스턴스로 운영 중이었는데 , 각 인스턴스의 Node.js 프로세스에서 주기적으로 GC가 발생할 때마다 서버로 들어오는 모든 요청 처리가 일시적으로 중단되었습니다. 이로 인해 약 30초 주기로 전체 서비스의 RPS(초당 요청 수)가 순간적으로 0에 가깝게 급락했고, 일부 요청은 504 Gateway Timeout 오류로 유실되는 문제가 발생했습니다.
  • 문제의 본질은 'GC의 영향 범위가 너무 크다'는 것이었습니다. 고사양 인스턴스 하나가 멈추면 그 파급력이 너무 컸습니다. 그래서 발상을 전환하여 'GC의 영향 범위를 구조적으로 분산시키고 격리'하는 수평 확장 전략을 선택했습니다.
    • 구조 변경: 기존의 고사양 인스턴스 17대를 vCPU 1개짜리 저사양 인스턴스 74대로 교체했습니다.
    • 작동 원리: 이렇게 구성하면 74개의 Node.js 프로세스가 완전히 독립적으로 동작합니다. 따라서 한 인스턴스에서 GC로 인해 'Stop-the-World'가 발생하더라도, 이는 오직 74개 중 1개의 서버에만 영향을 미칩니다. 나머지 73개의 인스턴스는 아무런 문제 없이 계속해서 트래픽을 처리합니다. 즉, 문제의 영향을 전체 서비스에서 개별 인스턴스 수준으로 축소시킨 것입니다.

Q4. 74대의 t2.small 인스턴스를 운영하셨는데, 이 많은 인스턴스를 어떻게 배포하고 관리하셨나요? (예: 자동화 스크립트, 오케스트레이션 툴 사용 여부)

Q5. CQRS 패턴을 적용해 읽기/쓰기 부하를 분리하셨습니다. 이 패턴을 적용하게 된 계기가 된 OOM(메모리 부족) 현상에 대해 더 자세히 설명해주시고, CQRS 구조에서 발생할 수 있는 데이터 정합성 문제는 어떻게 해결하셨나요?

  • (CQRS와 데이터 정합성): 읽기(대시보드 조회)와 쓰기(로그 수집) 요청이 같은 DB 노드에 몰리면서 메모리 부족(OOM)으로 서버가 중단되는 문제가 발생해 CQRS를 도입했습니다. 데이터 정합성은 ClickHouse의 Materialized View를 통해 해결했습니다. 쓰기 전용 노드에 데이터가 쌓이면(INSERT), MV가 이를 감지해 읽기 전용 노드가 바라보는 집계 테이블로 데이터를 자동으로 가공 및 전송해주기 때문에 두 노드 간의 데이터는 항상 동기화된 상태를 유지할 수 있었습니다.

  • 도입 계기 (OOM 현상): SDK에서 수집된 클릭 이벤트가 실시간으로 INSERT되는 쓰기 작업과, 대시보드에서 수많은 SELECT 쿼리를 실행하는 읽기 작업이 모두 동일한 ClickHouse 노드에 집중되면서 문제가 발생했습니다. 이 부하가 임계점을 넘어서는 순간 메모리 부족(OOM) 현상이 발생했고, 이는 데이터 수집 실패, 서버 중단, 심지어 ClickHouse 노드 장애로까지 이어졌습니다.

  • CQRS 아키텍처 설계: 이 문제를 해결하기 위해 명령(쓰기)과 조회(읽기)의 책임을 물리적으로 분리하는 CQRS 구조를 ClickHouse 레벨에서 직접 설계했습니다.

    • A 노드 (쓰기 전용): Kafka를 통해 들어오는 모든 로그를 INSERT하는 역할만 전담했습니다.
    • B 노드 (읽기 전용): 대시보드 API의 모든 SELECT 요청을 처리하며, 미리 가공된 집계 테이블만 조회하도록 제한했습니다.
  • 데이터 정합성 해결: 데이터 정합성은 ClickHouse의 Materialized View(MV)를 통해 해결했습니다. A 노드에 새로운 데이터가 써지면, MV가 자동으로 이 변경을 감지하여 B 노드가 조회하는 집계 테이블에 데이터를 가공하여 반영해줍니다. 즉, 데이터의 흐름이 A노드(원본) -> MV -> B노드(집계)로 이어지는 파이프라인을 통해 두 노드 간의 데이터가 거의 실시간으로 동기화되므로 정합성 문제를 해결할 수 있었습니다. 이 구조 덕분에 OOM 현상 없이 수집과 분석이 안정적으로 병행 가능해졌습니다.

Q6. 이벤트 수집 SDK에서 kl-disabled 클래스를 통해 불필요한 이벤트를 제외하는 기능을 구현하셨습니다. 클래스 방식 외에 다른 방식(예: 특정 속성 사용)도 고려해보셨나요? 이 방식의 장단점은 무엇이라고 생각하시나요?

  • (kl-disabled 클래스): CSS 클래스 방식은 개발자가 직관적으로 HTML 구조에 적용하기 편하다는 장점이 있었습니다. 이벤트 리스너에서 DOM 트리를 탐색해 부모 요소에 해당 클래스가 있는지 확인하는 로직을 추가하여, 상위 요소 하나에만 클래스를 적용해도 모든 하위 요소에 적용되도록 구현해 사용 편의성을 높였습니다.

Q7. 사용자가 직접 전환 이벤트를 정의하는 기능을 구현하셨는데, 사용자가 정의한 여러 복잡한 조건을 어떻게 데이터베이스에 저장하고, SDK에서 효율적으로 조회하여 필터링했는지 그 설계 구조가 궁금합니다.

  • (전환 이벤트 설계): 사용자가 대시보드에서 '특정 페이지 경로', '버튼 요소' 등을 조건으로 등록하면, 이 규칙(Rule)들을 데이터베이스에 저장했습니다. SDK는 웹사이트에 로드될 때 이 규칙들을 미리 가져오고, 사용자가 이벤트를 발생시킬 때마다 수집된 이벤트 정보가 저장된 규칙과 일치하는지 실시간으로 판별하여 전환 여부를 태깅하는 방식으로 구현했습니다.

Krafton Jungle 및 CS 기초

Q1. PintOS 프로젝트에서 가상 메모리를 구현하며 Lazy loading, Demand paging을 다루셨습니다. Demand Paging의 작동 원리와 이점, 그리고 이 과정에서 발생하는 페이지 폴트(Page Fault) 처리 흐름을 설명해주세요.

  • (Demand Paging): Demand Paging은 프로세스가 요청하는 메모리 페이지만 물리 메모리에 적재하는 기법입니다. 처음부터 모든 메모리를 올리지 않아도 되므로 프로세스 시작이 빠르고 메모리를 효율적으로 사용할 수 있습니다. 페이지 폴트는 CPU가 찾는 페이지가 물리 메모리에 없을 때 발생하는 인터럽트인데, 이때 운영체제는 디스크에서 해당 페이지를 찾아 비어있는 프레임에 적재하고, 페이지 테이블을 갱신한 뒤 중단되었던 명령을 다시 실행합니다.

  • Demand Paging 원리와 이점: Demand Paging은 프로세스 실행에 필요한 페이지를 요청이 있을 때만 물리 메모리에 올리는 기법입니다. 처음부터 프로세스의 모든 부분을 메모리에 올리지 않기 때문에 프로세스 시작 속도가 빠르고, 실제 사용하지 않는 페이지에 메모리를 낭비하지 않아 물리 메모리를 훨씬 효율적으로 사용할 수 있다는 큰 이점이 있습니다.

  • 페이지 폴트 처리 흐름: 페이지 폴트는 CPU가 접근하려는 페이지가 물리 메모리에 존재하지 않을 때 발생하는 하드웨어 인터럽트입니다. 제가 구현한 처리 흐름은 다음과 같습니다:

    1. 프로세스가 특정 메모리 주소에 접근을 시도합니다.
    2. MMU가 페이지 테이블을 참조했으나 해당 페이지가 물리 메모리에 없음을 확인하고 CPU에 트랩(Trap)을 발생시킵니다.
    3. CPU는 사용자 모드에서 커널 모드로 전환되고, 페이지 폴트 핸들러가 실행됩니다.
    4. 핸들러는 해당 메모리 접근이 유효한지(예: 할당된 영역인지) 확인합니다.
    5. 유효하다면, 디스크에서 필요한 페이지 데이터를 찾아 비어있는 물리 메모리 프레임에 로드(Lazy Loading)합니다.
    6. 페이지 테이블 정보를 새로 로드된 프레임 주소로 갱신합니다.
    7. 페이지 폴트를 발생시켰던 명령어를 다시 실행하면, 이번에는 정상적으로 메모리에 접근하게 됩니다.

Q2. 'priority donation' 기능을 도입하여 커널 동시성 문제를 해결하셨습니다. 이것이 해결하고자 하는 '우선순위 역전(Priority Inversion)' 문제가 무엇인지 설명해주실 수 있나요?

  • ('우선순위 역전'과 'priority donation') : '우선순위 역전'은 높은 우선순위의 작업(Task A)이 낮은 우선순위의 작업(Task C)이 점유한 자원을 기다리느라, 중간 우선순위의 작업(Task B)에 오히려 실행 순서가 밀리는 문제입니다. 'priority donation'은 이 문제를 해결하기 위해, 자원을 점유한 낮은 순위의 작업(C)에게 높은 순위 작업(A)의 우선순위를 일시적으로 '기부'하여 빨리 작업을 마치고 자원을 해제하도록 만드는 기법입니다.

Q3. 직접 malloc을 구현한 경험을 말씀해주셨는데, 어떤 메모리 할당 알고리즘(예: first-fit, next-fit, best-fit)을 사용하셨고, 그 이유는 무엇인가요? 구현하면서 가장 어려웠던 점은 무엇이었나요?

  • (malloc 구현): 이력서에 명시된 next-fit 알고리즘을 사용해 구현했습니다. next-fit은 마지막으로 할당된 위치부터 탐색을 시작하여 메모리 전체를 매번 탐색하는 first-fit보다 탐색 오버헤드를 줄일 수 있을 것이라 기대했습니다. 구현 시 가장 어려웠던 점은 할당된 메모리 블록들을 효율적으로 관리하기 위한 가용 리스트(free list) 자료구조 설계와, 메모리 단편화를 줄이기 위해 인접한 빈 블록들을 합치는 병합(Coalescing) 로직을 정확하게 구현하는 부분이었습니다.

기술 스택 및 개발 경험

Q1. 이력서를 보면 ClickHouse, PostgreSQL, MySQL, MongoDB 등 다양한 데이터베이스 사용 경험이 있습니다. 각 데이터베이스의 특징은 무엇이며, 어떤 상황에서 특정 데이터베이스를 선택하는 것이 유리할지 설명해주세요. KlickLab에서 ClickHouse를 선택한 이유는 무엇이었나요?

  • 각 데이터베이스는 목적에 맞게 선택해야 합니다. MySQL/PostgreSQL은 트랜잭션이 중요하고 데이터 구조가 정형적일 때, MongoDB는 스키마가 유연하고 수평 확장이 필요할 때 적합합니다. ClickHouse는 대용량 데이터를 빠르게 읽고 집계하는 분석(OLAP) 환경에 특화되어 있습니다. KlickLab은 수억 건의 로그를 실시간으로 분석하는 플랫폼이었기 때문에, 행 기반 데이터베이스보다 열 기반으로 동작하여 분석 쿼리에 압도적인 성능을 보이는 ClickHouse를 선택했습니다.

Q2. 키오스크 앱의 대용량 파일 업로드 문제를 분할 업로드로 해결하셨습니다. 클라이언트에서 파일을 나누고 서버에서 병합하는 과정에 대해 설명해주시고, 만약 중간에 특정 조각의 업로드가 실패했을 때 어떻게 재시도 로직을 구현하셨는지 궁금합니다.

  • (분할 업로드): 클라이언트에서 .apk 파일을 지정된 크기(chunk)로 잘라 순서대로 서버에 전송합니다. 서버는 각 조각을 임시 파일에 순차적으로 이어 붙입니다. 재시도 로직은, 각 조각의 업로드 성공 여부를 저장하고 네트워크 연결이 끊겼다가 다시 이어지면, 클라이언트가 서버에 마지막으로 성공한 조각이 몇 번째인지 물어보고 그 다음 조각부터 전송을 재개하는 방식으로 구현했습니다.
  1. 분할 업로드 및 병합 과정
    • 클라이언트: 사용자가 500MB 크기의 .apk 파일을 업로드하면, 클라이언트(브라우저)의 자바스크립트가 이 파일을 5MB 같은 일정한 크기의 조각(chunk)으로 나눕니다. 그리고 각 조각에 순서 번호를 붙여 서버로 순차적으로 전송합니다.
    • 서버: 서버는 전송받은 조각들을 임시 디렉토리에 파일명_조각번호 형식으로 저장합니다. 모든 조각이 도착하면, 서버는 조각 번호 순서대로 파일들을 하나로 합쳐서 완전한 .apk 파일로 복원합니다.
  2. 재시도 로직 (이어 올리기): 이 기능의 핵심은 '이어 올리기'가 가능하다는 점입니다.
    • 상태 저장: 클라이언트는 각 조각을 전송하고 서버로부터 성공 응답을 받을 때마다 '몇 번 조각까지 성공했는지'를 로컬 스토리지 같은 곳에 기록합니다. 서버 역시 어떤 파일의 몇 번 조각까지 받았는지 기록해 둡니다.
    • 업로드 재개: 네트워크 오류 등으로 업로드가 중단되었다가 재개될 경우, 클라이언트는 서버에 마지막으로 성공한 조각 번호를 물어보거나 자신의 기록을 확인합니다. 그리고 실패했거나 아직 보내지 않은 그 다음 조각부터 전송을 다시 시작합니다. 예를 들어 100개 조각 중 50번까지 성공했다면, 51번 조각부터 다시 업로드를 시작하는 것입니다.

Q3. 프로젝트 진행 시 동료들과의 코드 리뷰나 토론을 통해 "왜 이 구조로 구현해야 하는가"를 설득하는 경험을 강조하셨습니다. 기술적인 의견 충돌이 있었던 구체적인 사례와, 어떻게 동료를 설득하거나 합의점을 찾았는지 이야기해주세요.

  • (기술적 의견 충돌): (가상 시나리오) KlickLab의 API 응답 캐싱 로직을 구현할 때, 한 동료는 모든 API에 일괄적으로 1분의 TTL을 가진 캐시를 적용하자고 제안했습니다. 저는 '실시간성이 중요한 지표'와 '하루 단위로 변하는 지표'가 다르므로, 각 API의 특성에 맞게 캐시 시간을 다르게 설정해야 한다고 주장했습니다. 근거로, 불필요하게 짧은 캐시는 DB 부하를 줄이지 못하고, 너무 긴 캐시는 사용자에게 잘못된 데이터를 보여줄 수 있다는 점을 설명했습니다. 결국 API의 데이터 갱신 주기를 기준으로 캐시 정책을 세분화하는 것으로 합의하여 더 정교한 시스템을 만들 수 있었습니다.

'공부' 카테고리의 다른 글

[Operation Systems] IPC  (0) 2025.05.01
[Algorithm] LCA  (0) 2025.04.08
[Algorithm] Segment Tree  (0) 2025.04.08
[Algorithm] Binary Tree  (0) 2025.04.08
[Algorithm] Floyd-Warshall  (0) 2025.03.31