📚FOS Study
홈카테고리
홈카테고리

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agents 페이지로 이동
    • langgraph 페이지로 이동
    • BMAD Method — AI 에이전트로 애자일 개발하는 방법론
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • Claude Code를 11일 동안 쓴 결과 — 데이터로 본 나의 사용 패턴
    • Claude Code 멀티 에이전트 — Teams
    • 하네스 엔지니어링 실전 — 4인 에이전트 팀으로 코딩 파이프라인 구축하기
    • 하네스 엔지니어링 — 오래 실행되는 AI 에이전트를 위한 설계
    • 멀티모달 LLM (Multimodal Large Language Model)
    • AI 에이전트와 함께 MVP 만들기 — dooray-cli 사례
  • algorithm 페이지로 이동
    • live-coding 페이지로 이동
  • architecture 페이지로 이동
    • 캐시 설계 전략 총정리
    • 디자인 패턴
    • 분산 트랜잭션
  • css 페이지로 이동
    • FlexBox 페이지로 이동
  • database 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • redis 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • 커넥션 풀 크기는 얼마나 조정해야할까?
    • 인덱스 - DB 성능 최적화의 핵심
    • 역정규화 (Denormalization)
    • 데이터 베이스 정규화
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • monitoring 페이지로 이동
    • Envoy Proxy
    • Graceful Shutdown
  • go 페이지로 이동
    • Go 언어 기본 학습
  • http 페이지로 이동
    • HTTP Connection Pool
  • interview 페이지로 이동
    • 210812 페이지로 이동
    • company-analysis 페이지로 이동
    • experience-based 페이지로 이동
    • 뱅크샐러드 AI Native Server Engineer
    • CJ 올리브영 커머스플랫폼유닛 Back-End 개발 지원 자료
    • 마이리얼트립 - Platform Solutions실 회원주문개발 Product Engineer
    • NHN 서비스개발센터 AI서비스개발팀
    • nhn gameenvil console backend 직무 인터뷰 준비
    • 면접을 대비해봅시다
    • 토스증권 Server Developer (Platform) 지원 자료
    • 토스증권 Server Developer (Product) 지원 자료
    • 토스뱅크 Server Developer (Product) 지원 자료
    • Tossplace Node.js Developer
    • 토스플레이스 Node.js 백엔드 컬처핏
  • java 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • 더_자바_코드를_조작하는_다양한_방법 페이지로 이동
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • OpenTelemetry 란 무엇인가?
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • Data_Structures_and_Algorithms 페이지로 이동
    • Heap 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node.js
    • npm vs pnpm 선택기준은 무엇인가요?
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 기본
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • [초안] Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • 메시지 전송 신뢰성
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • tmux — Terminal Multiplexer
  • network 페이지로 이동
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • react 페이지로 이동
    • JSX 페이지로 이동
    • VirtualDOM 페이지로 이동
    • v16 페이지로 이동
  • resume 페이지로 이동
    • 지원 문항
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
📚FOS Study

개발 학습 기록을 정리하는 블로그입니다.

바로가기

  • 홈
  • 카테고리

소셜

  • GitHub
  • Source Repository

© 2025 FOS Study. Built with Next.js & Tailwind CSS

목록으로 돌아가기
💼interview/ experience-based

[초안] CJ 올리브영 면접 대비: 실전 경험 기반 질문 답변 준비 시트

약 21분
2026년 4월 17일
GitHub에서 보기

[초안] CJ 올리브영 면접 대비: 실전 경험 기반 질문 답변 준비 시트

작성일: 2026-04-16 | 면접일: 2026-04-21 | 포지션: 커머스플랫폼유닛 Back-End 개발


내 경험 내러티브: 3분 자기소개 뼈대

NHN에서 4년 동안 소셜 카지노 게임 백엔드와 사내 AI 서비스 파이프라인을 담당했다. 내가 일해온 방식을 한 문장으로 요약하면 "기능이 동작하는 것에서 그치지 않고, 팀 전체가 오래 유지보수할 수 있는 구조를 만드는 것" 이다.

테마 1: 다중 서버 환경에서의 정합성 문제

정적 설정 데이터를 애플리케이션 메모리에 캐싱해 DB 부하를 줄였다. 어드민에서 데이터가 변경·커밋되면 Hibernate PostCommitUpdateEventListener가 발동해 RabbitMQ Fanout Exchange로 변경된 게임 ID를 브로드캐스트한다. 각 서버 인스턴스는 자신의 큐에서 메시지를 수신해 해당 데이터만 선택적으로 갱신한다. 갱신 도중 다른 스레드의 조회 요청이 들어오면 일시적 NPE가 발생했는데, StampedLock의 writeLock으로 갱신 구간을 보호하고 tryReadLock에 2.5초 타임아웃을 걸어 해결했다.

테마 2: 비동기 이벤트 드리븐 설계

스핀 API의 응답 흐름을 동기/비동기로 분리했다. 금액 처리·레벨 변화처럼 즉시 응답이 필요한 로직은 DB 트랜잭션 안에서 처리하고, 미션 진행·통계·알림은 Kafka 비동기로 넘겼다. @TransactionalEventListener(AFTER_COMMIT)으로 커밋 이후에만 메시지가 발행되도록 보장했고, Kafka 전송 실패 시 Outbox Pattern으로 메시지 유실을 막았다.

테마 3: 대용량 데이터 처리 파이프라인

AI 서비스 팀으로 이동해 사내 문서를 OpenSearch에 벡터 색인하는 Spring Batch 파이프라인을 처음부터 설계했다. 11개 Step 분리, AsyncItemProcessor를 활용한 I/O 병렬화, CompositeItemProcessor 4단계 체이닝, 커서 기반 재시작 구조까지 전 과정을 직접 구현했다.

올리브영과의 연결 포인트

올리브영 기술 블로그에서 Cache-Aside + Kafka Event-Driven 하이브리드 설계, Resilience4j 기반 장애 격리, Feature Flag 기반 무중단 배포를 다룬다는 걸 확인했다. 내가 해온 캐시 정합성 설계, Kafka Outbox Pattern, 대용량 배치 파이프라인은 1,600만 고객 규모의 커머스 트래픽 환경에 직접 맞닿는 경험이다.


핵심 질문 1: 다중 서버 인메모리 캐시 정합성

예상 질문 형태

"여러 서버 인스턴스가 뜬 환경에서 캐시 데이터를 어떻게 동기화했나요? 갱신 중 동시성 문제는 어떻게 처리했나요?"

면접관이 실제로 보는 것

  • 단순 Redis TTL 캐싱을 넘어서, 분산 환경에서 캐시 정합성 문제를 실제 겪고 해결한 경험이 있는가
  • 동시성 도구(Lock)를 선택하는 근거를 설명할 수 있는가. ReentrantReadWriteLock, StampedLock, synchronized 차이를 이해하는가
  • "정합성을 희생해 성능을 얻는" 트레이드오프를 의식적으로 다루는가

답변에 반드시 포함할 근거 · 수치 · 사례

항목구체적 내용
캐싱 목적정적 설정 데이터(릴 테이블, 심볼 설정)를 JVM 힙에 올려 DB 부하 제거
전파 방식Hibernate PostCommitUpdateEventListener → RabbitMQ Fanout Exchange → 각 인스턴스 큐 → 해당 game ID만 선택적 갱신
문제 상황refreshAll() 진행 중 스레드2 조회 → 기존 데이터 clear 후 새 데이터 로드 전 공백 구간에서 NPE
해결 방식StampedLock: 갱신 시 writeLock으로 읽기 차단, 조회 시 tryReadLock(2.5초 타임아웃)
설계 포인트StaticDataManager 인터페이스로 데이터 타입별 init/refresh/clear 책임 분리 → 신규 캐시 타입 추가 시 기존 코드 변경 없음

피해야 할 약한 답변 패턴

  • "Redis 쓰면 되지 않나요?"라는 질문에 답하지 못하는 상황 → 왜 Redis가 아닌 인메모리인지 먼저 설명해야 한다 (조회 속도, 네트워크 홉 제거, 변경 빈도가 낮은 정적 데이터)
  • "RabbitMQ로 알림 보냈다"고만 말하고 갱신 중 동시성 문제를 언급하지 않으면 깊이 부족으로 보인다
  • StampedLock을 "그냥 락 걸었다"로 설명하면 설계 의도가 사라진다. tryReadLock 타임아웃 값 결정 근거(갱신 소요 시간 측정 후 버퍼 추가)까지 설명해야 한다
  • "이벤트 수신 실패 시 어떻게 되나요?"에 대한 준비 없으면 약점 노출 → RabbitMQ는 메시지 보존이 되므로 재연결 후 소비 가능, 최악의 경우 서버 재시작 시 DB에서 전체 재초기화

압축 답변 구조 (STAR)

S(상황): 다중 서버 환경에서 슬롯 게임의 정적 설정 데이터를 JVM 인메모리에 캐싱해 DB 부하를 최소화하는 구조였습니다.

T(문제): 어드민에서 설정이 변경됐을 때 모든 서버 인스턴스의 캐시를 정확히 동기화해야 했고, 갱신 도중 다른 스레드의 조회 요청이 들어오면 NPE가 발생하는 동시성 문제가 있었습니다.

A(해결): Hibernate PostCommitUpdateEventListener가 커밋 이후에만 발동해 RabbitMQ Fanout Exchange로 변경된 게임 ID를 브로드캐스트하도록 설계했습니다. 동시성 문제는 StampedLock을 도입해 갱신 시 writeLock으로 읽기를 차단하고, 조회 시 tryReadLock에 2.5초 타임아웃을 걸어 갱신이 완료될 때까지 대기하도록 해결했습니다. StaticDataManager 인터페이스로 데이터 타입별 책임을 분리해 확장 구조도 갖췄습니다.

R(결과): 갱신 중 일시적 정합성 오류가 해소됐고, 신규 캐시 타입 추가 시 기존 코드를 건드리지 않는 구조를 만들었습니다.

꼬리 질문 5개

Q1. StampedLock을 ReentrantReadWriteLock 대신 선택한 이유는 무엇인가요?

StampedLock은 낙관적 읽기(tryOptimisticRead)를 지원해서, 쓰기 락 없이도 읽기를 시도하고 이후 검증하는 방식이 가능합니다. 저의 케이스에서는 갱신 빈도가 낮고 읽기가 압도적으로 많은 구조라서 tryReadLock을 사용했습니다. ReentrantReadWriteLock은 쓰기 락이 걸리면 읽기 락을 아예 막는 반면, StampedLock은 좀 더 세밀한 제어가 가능합니다. 단, StampedLock은 재진입(reentrant)을 지원하지 않아서 같은 스레드에서 중첩 락 획득 시 주의가 필요합니다. 이 점을 감안해 락 범위를 최소한으로 유지했습니다.

Q2. RabbitMQ Fanout Exchange를 선택한 이유와 Redis Pub/Sub과의 차이는?

이미 RabbitMQ 인프라가 팀에 있었고, Fanout Exchange는 Exchange에 바인딩된 모든 큐에 메시지를 자동으로 복제 전달하는 기능이 기본 제공됩니다. 서버 인스턴스가 늘어나도 각 인스턴스가 자신의 큐를 갖기 때문에 코드 변경 없이 확장됩니다. Redis Pub/Sub도 비슷하게 쓸 수 있지만, Pub/Sub은 연결이 끊긴 구독자는 메시지를 유실합니다. RabbitMQ는 큐에 메시지를 보존하기 때문에 인스턴스가 잠시 다운됐다가 재연결해도 발행된 메시지를 소비할 수 있습니다.

Q3. tryReadLock의 2.5초 타임아웃 값은 어떻게 결정했나요?

실제 갱신 작업의 소요 시간을 측정했습니다. clearAllStaticData()와 DB에서 데이터를 다시 로드하는 데 걸리는 시간을 측정하고, 여기에 충분한 버퍼를 더한 값입니다. 너무 짧으면 갱신이 완료되기 전에 락 획득에 실패해 요청이 예외를 받고, 너무 길면 실제 장애 상황에서 요청이 타임아웃까지 대기하게 됩니다. 갱신 빈도가 낮고 갱신 소요 시간이 예측 가능한 정적 데이터였기 때문에 이 방식이 적합했습니다.

Q4. 캐시 갱신 이벤트 수신에 실패하면 어떻게 되나요?

RabbitMQ는 큐에 메시지를 보존하기 때문에 인스턴스가 재연결하면 밀린 메시지를 소비합니다. 인스턴스가 완전히 재시작되는 경우에는 startup 시 DB에서 전체 데이터를 다시 초기화하는 로직이 있어서, 이전 갱신 이벤트를 놓쳐도 최신 상태로 복원됩니다. 다만, 인스턴스 재시작 없이 장시간 메시지 소비가 지연되면 일시적으로 오래된 캐시 데이터를 서빙할 수 있습니다. 이 케이스는 정적 설정 데이터의 특성상 비즈니스 허용 범위 안이었습니다.

Q5. 이 구조를 커머스 환경의 상품 재고나 가격 캐시에 적용한다면 달라지는 점이 있나요?

달라지는 핵심은 변경 빈도와 일관성 요구 수준입니다. 슬롯 설정 데이터는 하루에 수 회 정도 바뀌는 반면, 커머스의 재고나 가격은 훨씬 자주 변경됩니다. 변경 빈도가 높아지면 Fanout Exchange 전파 지연 동안 오래된 캐시가 서빙되는 구간이 길어져서 문제가 됩니다. 올리브영 기술 블로그에서처럼 Cache-Aside 방식으로 TTL을 짧게 유지하거나, 재고처럼 정확성이 중요한 데이터는 캐시에서 조회 후 DB 확인을 병행하는 방식을 고려해야 합니다.


핵심 질문 2: Kafka Transactional Outbox Pattern

예상 질문 형태

"Kafka를 사용한 비동기 처리에서 메시지 유실 없이 처리를 보장한 경험이 있나요? 트랜잭션과 메시지 발행 시점을 어떻게 관리했나요?"

면접관이 실제로 보는 것

  • @TransactionalEventListener 동작 원리와 AFTER_COMMIT을 쓰는 이유를 이해하는가 (DB 롤백 후 Kafka 메시지가 나가는 문제를 인식하는가)
  • Outbox Pattern을 교과서가 아닌 실제 문제에서 도출해 설계한 경험이 있는가
  • at-least-once 보장과 멱등성 처리 사이의 트레이드오프를 의식하는가

답변에 반드시 포함할 근거 · 수치 · 사례

항목구체적 내용
응답 흐름 분리 기준즉시 응답 필요: 금액 처리·레벨 변화 → DB 트랜잭션 안에서 동기 처리
최종 일관성으로 충분: 미션 진행·통계·알림 → Kafka 비동기
AFTER_COMMIT 이유BEFORE_COMMIT으로 발행 시 DB 롤백이 발생해도 Kafka 메시지는 나간 상태가 됨
Outbox 설계Kafka 전송 실패 → REQUIRES_NEW 별도 트랜잭션으로 실패 메시지 DB 저장 → 스케줄러 재전송
traceId실패 메시지에 traceId 함께 저장해 원인 추적 가능

피해야 할 약한 답변 패턴

  • "Kafka는 at-least-once라서 Consumer에서 멱등성만 처리하면 된다"고만 말하면 Producer 측 유실 가능성을 간과하는 것으로 보인다. 트랜잭션 커밋은 성공했는데 Kafka 전송이 실패하는 케이스가 Outbox Pattern의 존재 이유다.
  • "@TransactionalEventListener 썼다"고만 말하고 왜 AFTER_COMMIT이어야 하는지 설명 못하면 깊이가 없다. "BEFORE_COMMIT으로 발행하면 롤백 시 어떻게 되는가"라는 꼬리 질문에 답하지 못하게 된다.
  • "Propagation.REQUIRES_NEW를 왜 썼나요?"에 답하지 못하면 약하다 → 메인 트랜잭션이 롤백되면 같은 트랜잭션 안의 실패 메시지 저장도 롤백되므로 별도 트랜잭션 필요.

압축 답변 구조 (STAR)

S(상황): 슬롯 스핀 API 하나에 금액 처리, 미션 업데이트, 통계, 알림이 모두 포함되어 트랜잭션이 너무 크고 응답 시간도 느렸습니다.

T(문제): 응답 흐름을 분리하면서 Kafka 메시지 발행 시점과 DB 커밋의 원자성을 보장해야 했습니다. DB 커밋 전에 메시지가 나가거나, 커밋 후 Kafka 전송이 실패하면 데이터 불일치가 생깁니다.

A(해결): @TransactionalEventListener(AFTER_COMMIT)으로 커밋 이후에만 메시지가 발행되도록 했습니다. Kafka 전송 실패 시에는 Propagation.REQUIRES_NEW 별도 트랜잭션으로 실패 메시지를 DB에 저장하고, 스케줄러가 주기적으로 재전송하는 Outbox Pattern을 구현했습니다. traceId도 함께 저장해 실패 원인을 추적할 수 있도록 했습니다.

R(결과): 동기/비동기 흐름을 명확히 분리해 응답 시간을 줄이고, 메시지 유실을 막는 구조를 갖췄습니다.

꼬리 질문 5개

Q1. BEFORE_COMMIT과 AFTER_COMMIT의 차이가 실제로 어떤 문제를 만드나요?

BEFORE_COMMIT으로 Kafka 메시지를 발행하면, 발행 직후 예외가 발생해 DB 트랜잭션이 롤백되어도 Kafka 메시지는 이미 브로커에 전달된 상태가 됩니다. Consumer는 DB에 반영되지 않은 이벤트를 처리하게 됩니다. AFTER_COMMIT을 쓰면 DB 커밋이 완료된 시점에 메시지가 발행되므로 이 문제를 방지할 수 있습니다. 단, AFTER_COMMIT 시점에 Kafka 발행이 실패하면 DB는 커밋됐는데 메시지는 없는 상황이 됩니다. 바로 이 케이스를 Outbox Pattern으로 처리했습니다.

Q2. Outbox 테이블에서 재전송 스케줄러의 주기와 재시도 횟수는 어떻게 관리했나요?

재시도 횟수 컬럼을 Outbox 레코드에 관리해서, 일정 횟수를 초과하면 더 이상 재시도하지 않고 별도로 집계해 모니터링할 수 있도록 했습니다. 스케줄러 주기는 비즈니스 SLA에 따라 설정하고, traceId를 통해 어떤 요청에서 발생한 실패인지 추적할 수 있게 했습니다. 알림처럼 실시간성이 중요한 이벤트는 짧은 주기로, 통계처럼 지연이 허용되는 이벤트는 긴 주기로 분리하는 방향도 고려했습니다.

Q3. Consumer에서 중복 메시지가 들어올 때 어떻게 처리했나요?

at-least-once 특성상 Consumer는 멱등하게 설계했습니다. 미션 진행 이벤트는 상태 머신 기반으로 이미 처리된 미션 ID에 대한 재처리를 방지합니다. 통계의 경우 idempotency key(예: spinId) 기반의 upsert 패턴으로 같은 이벤트가 중복 처리되어도 결과가 동일하도록 설계했습니다.

Q4. 동기 처리(금액/레벨)와 비동기 처리(미션/통계)를 나누는 기준이 무엇이었나요?

두 가지 기준이 있었습니다. 첫 번째는 즉각적인 응답 필요 여부입니다. 금액과 레벨은 스핀 응답에 포함되어 클라이언트가 즉시 보여줘야 하지만, 통계와 알림은 약간 지연되어도 사용자 경험에 영향이 없습니다. 두 번째는 장애 시 롤백 영향 범위입니다. 동기 흐름 안에 너무 많은 로직이 있으면 하나가 실패할 때 전체가 롤백됩니다. 비즈니스 핵심인 금액 처리만 동기 트랜잭션 안에 두고, 부가 처리는 분리해 장애 전파를 최소화했습니다.

Q5. Kafka 브로커가 완전히 다운된 상황에서 Outbox Pattern이 어떻게 동작했나요?

Kafka 브로커가 다운되면 전송 시도가 실패하고 예외가 발생합니다. AFTER_COMMIT 이후 발행 실패이므로 DB 트랜잭션은 이미 커밋된 상태입니다. 이때 REQUIRES_NEW 트랜잭션으로 실패 메시지를 Outbox 테이블에 저장합니다. 스케줄러는 Kafka 가용성과 관계없이 주기적으로 Outbox 테이블을 조회해 재전송을 시도합니다. 브로커가 복구되면 쌓인 메시지들이 순차적으로 재전송됩니다.


핵심 질문 3: Spring Batch 대용량 파이프라인 설계

예상 질문 형태

"대용량 데이터를 처리하는 배치 작업을 설계한 경험이 있나요? 실패 격리, 재시작, 성능 최적화를 어떻게 처리했나요?"

면접관이 실제로 보는 것

  • Spring Batch 청크 지향 처리의 동작 원리를 실제 코드로 설명할 수 있는가
  • I/O 바운드 병목을 인식하고 적절한 비동기 처리 방식을 적용했는가
  • Step 분리가 단순 코드 정리가 아닌 운영 관점의 실패 격리 전략임을 이해하는가

답변에 반드시 포함할 근거 · 수치 · 사례

항목구체적 내용
파이프라인 규모수천 개 Confluence 문서 → OpenSearch 벡터 색인, 11개 Step 분리
병목 분석임베딩 API 호출은 네트워크 I/O, 동기 처리 시 청크(10개) 처리에 최소 2초 소요
해결 방식AsyncItemProcessor + AsyncItemWriter로 청크 내 문서를 스레드풀에서 병렬 처리
비용 절감ChangeFilterProcessor: Confluence version 필드 비교 → 미변경 문서 임베딩 API 호출 스킵
Step 간 데이터 공유@JobScope 빈 사용 (JobExecutionContext는 청크 커밋마다 DB 직렬화 → 수천 개 ID 저장 부적합)
재시작ItemStream 구현으로 커서 위치를 ExecutionContext에 저장, 실패 지점부터 재시작

피해야 할 약한 답변 패턴

  • "Step을 많이 나눴다"고만 말하고 왜 나눴는지 설명 못하면 약하다. 11개 Step을 나눈 핵심은 실패 격리다. 댓글 색인 Step이 실패해도 페이지 색인 Step 결과는 보존되고, 재시작 시 댓글 Step부터 이어서 실행된다.
  • AsyncItemProcessor의 동작 원리를 "비동기로 처리한다"로만 말하면 부족하다. Processor가 Future를 반환하고, Writer 단계의 AsyncItemWriter가 Future.get()을 호출해 결과를 모아 벌크 처리하는 흐름을 설명해야 한다.
  • "@JobScope와 @StepScope 차이가 무엇인가요?" 질문에 답하지 못하면 약하다.

압축 답변 구조 (STAR)

S(상황): 사내 AI 서비스의 RAG 기능을 위해 수천 개 Confluence 문서를 OpenSearch에 벡터 색인해야 했습니다. 임베딩 API 호출이 네트워크 I/O라 동기 처리 시 매우 느렸습니다.

T(문제): 처리 속도 개선, 중간 실패 시 재시작 가능한 구조, 미변경 문서의 불필요한 임베딩 API 호출 방지가 필요했습니다.

A(해결): 수집·변환·임베딩·색인·삭제 동기화를 11개 Step으로 분리해 실패 격리를 구현했습니다. 임베딩 API 병목은 AsyncItemProcessor로 청크 내 문서를 스레드풀에서 병렬 처리해 해결했습니다. ChangeFilterProcessor에서 버전 비교로 미변경 문서를 스킵하고, ItemStream으로 커서 위치를 저장해 실패 지점부터 재시작할 수 있도록 했습니다.

R(결과): I/O 병렬화로 처리 속도를 단축했고, Step 단위 실패 격리로 부분 실패 시에도 이전 결과를 보존할 수 있는 구조를 갖췄습니다.

꼬리 질문 5개

Q1. AsyncItemProcessor는 어떻게 동작하는지 구체적으로 설명해주세요.

AsyncItemProcessor는 각 아이템을 TaskExecutor(스레드풀)에 제출하고 즉시 Future를 반환합니다. 청크 내 모든 아이템이 Future로 감싸져서, 실제 I/O 작업은 스레드풀에서 병렬로 실행됩니다. Writer 단계에서는 AsyncItemWriter가 각 Future에 대해 get()을 호출해 결과를 기다린 뒤, 모은 결과를 실제 Writer(OpenSearch 벌크 색인)에 위임합니다. 스레드풀 크기는 I/O 특성에 맞게 CPU 코어 수보다 크게 설정했는데, I/O 대기 시간이 길수록 더 많은 스레드가 동시에 I/O를 기다릴 수 있기 때문입니다.

Q2. @JobScope 빈에 Step 간 공유 데이터를 저장한 이유가 무엇인가요?

처음에는 JobExecutionContext에 페이지 ID 목록을 저장했습니다. 그런데 JobExecutionContext는 청크 커밋마다 BATCH_JOB_EXECUTION_CONTEXT 테이블에 직렬화됩니다. 수천 개의 페이지 ID를 청크마다 DB에 읽고 쓰는 건 불필요한 I/O 부하입니다. @JobScope 빈은 Job 실행 단위로 생성되는 메모리 내 싱글톤이라 DB 직렬화 없이 데이터를 공유할 수 있습니다. JobExecutionContext는 재시작을 위한 커서 위치처럼 꼭 영속해야 하는 경량 상태에만 씁니다.

Q3. 재시작 시 @JobScope 빈이 비어있는 문제는 어떻게 해결했나요?

Job 실패 후 재시작하면 새 JobExecution이 생성되고 @JobScope 빈도 새 인스턴스로 초기화됩니다. 그런데 상태를 채우는 Step들이 이전 실행에서 COMPLETED 처리되어 스킵되면, 빈이 빈 상태로 남아서 NPE가 발생합니다. 해결 방법으로 상태 로더 Step에 allowStartIfComplete(true)를 설정해서, 재시작 시에도 해당 Step이 반드시 재실행되게 했습니다. 이 Step들은 실행 시간이 짧아서 재실행 비용이 크지 않습니다.

Q4. CompositeItemProcessor의 4단계 중 ChangeFilter를 가장 앞에 둔 이유는?

이후 단계(데이터 보강, ADF→Markdown 변환, 임베딩 API 호출)가 모두 외부 API 호출을 포함하는 비용이 큰 작업입니다. 버전 비교로 미변경 문서를 가장 먼저 걸러내면, 나머지 단계를 아예 건너뛸 수 있습니다. 실제로 대부분의 배치 실행에서 전체 문서 중 일부만 변경되기 때문에, ChangeFilter를 앞에 두는 것만으로도 임베딩 API 호출 횟수를 크게 줄일 수 있었습니다.

Q5. 스페이스마다 메타데이터 포맷이 다를 때 기존 코드를 어떻게 건드리지 않고 처리했나요?

ConfluenceDocumentMetadataProvider 인터페이스로 메타데이터 빌드 로직을 추상화했습니다. 기본 포맷을 위한 DefaultConfluenceDocumentMetadataProvider와 신규 스페이스 포맷을 위한 별도 구현체를 만들고, 각 배치 잡 Config에서 @Qualifier로 원하는 구현체를 주입합니다. EmbeddingProcessor는 인터페이스에만 의존하기 때문에 신규 스페이스 추가 시 EmbeddingProcessor 코드를 건드릴 필요가 없습니다. 전략 패턴을 처음부터 도입했기 때문에 나중에 스페이스가 추가됐을 때 수월하게 대응할 수 있었습니다.


핵심 질문 4: 도메인 로직 추상화 및 슬롯 엔진 구조 개선

예상 질문 형태

"복잡하게 얽힌 비즈니스 로직을 어떻게 정리하고 구조화했나요? JPA나 Hibernate를 활용하면서 도메인 설계에서 어떤 선택을 했나요?"

면접관이 실제로 보는 것

  • 단순 CRUD를 넘어서 도메인 레이어에서 비즈니스 로직을 설계하는 경험이 있는가 (올리브영 필수 요건: JPA/Hibernate ORM 활용 및 도메인 모델링)
  • 레거시 코드를 리팩터링할 때 회귀 방지를 위한 테스트 전략을 갖추고 있는가
  • OCP, Template Method, Strategy 같은 설계 원칙을 실제 문제에서 적용했는가

답변에 반드시 포함할 근거 · 수치 · 사례

항목구체적 내용
리팩터링 전 문제슬롯 5종 이상 쌓이면서 각 슬롯이 공통 흐름을 직접 구현, SlotConfigFactory가 모든 슬롯 케이스 보유
Template MethodAbstractPlayService로 스핀 공통 흐름 통합, SpinOperationHandler 인터페이스로 타입별 동작 위임
OCP 적용ExtraConfig 생성 책임을 각 슬롯에 위임 → 신규 슬롯 추가 시 SlotConfigFactory 수정 불필요
Hibernate 활용PostCommitUpdateEventListener로 커밋 이후 자동으로 캐시 갱신 트리거
테스트 인프라447개 테스트 파일, AbstractSlotTest 제네릭 추상 클래스로 게임 타입별 초기화 자동화
DI 전환static 의존 → DI로 전환해 테스트에서 모킹 가능하게

피해야 할 약한 답변 패턴

  • "추상 클래스 만들었다"고만 말하고 왜 interface default 메서드가 한계인지 설명 못하면 약하다. interface default 메서드는 상태를 가질 수 없다는 핵심을 짚어야 한다.
  • static 메서드를 DI로 전환한 이유를 "코드가 지저분해서"로 설명하면 설계 이해가 없는 것처럼 보인다. 테스트 가능성(모킹 불가 문제)과 직접 연결해야 한다.
  • Hibernate PostCommitUpdateEventListener를 "JPA 이벤트 리스너를 썼다"로만 설명하면 깊이가 없다. 왜 PostCommit이어야 하는지(커밋 전 발동하면 롤백 시 캐시가 갱신된 상태로 남는다)를 설명해야 한다.

압축 답변 구조 (STAR)

S(상황): 슬롯이 5종 이상 쌓이면서 각 슬롯이 공통 처리 흐름(베팅 검증, 스핀 결과 계산, 후처리)을 직접 구현하고 있었고, 모든 슬롯의 설정 생성 책임이 SlotConfigFactory 하나에 몰려 있었습니다.

T(문제): 새 슬롯을 추가할 때마다 다른 슬롯의 코드를 건드려야 하고, 테스트가 없어서 리팩터링이 두려운 상황이었습니다.

A(해결): AbstractPlayService 단일 템플릿으로 공통 흐름을 통합하고, 타입별로 달라지는 동작은 SpinOperationHandler 인터페이스로 위임했습니다. ExtraConfig 생성 책임을 각 슬롯에 위임해 OCP를 적용했고, static 의존을 DI로 전환해 테스트 가능성을 높였습니다. AbstractSlotTest 제네릭 추상 클래스로 테스트 인프라를 만들어 총 447개 테스트 파일에서 핵심 로직을 커버했습니다.

R(결과): 이후 슬롯 3종을 추가할 때 기존 코드를 건드리지 않았고, 테스트 안전망이 있어 리팩터링과 신규 기능 추가를 빠르게 진행할 수 있었습니다.

꼬리 질문 5개

Q1. interface default 메서드 대신 추상 클래스를 선택한 구체적인 이유는 무엇인가요?

SlotService 인터페이스에 default 구현이 늘어나면서 두 가지 문제가 생겼습니다. 첫째, interface default 메서드는 인스턴스 필드를 가질 수 없어서 공통 로직에 필요한 스프링 빈 의존성을 주입받을 방법이 없었습니다. 둘째, default 메서드는 상태를 가질 수 없어서 여러 메서드 사이에 공유해야 하는 중간 계산 결과를 보관할 곳이 없었습니다. BaseSlotService 추상 클래스로 옮기면 생성자 주입과 필드를 쓸 수 있어서 이 문제가 해결됩니다.

Q2. PostCommitUpdateEventListener를 쓴 이유는 무엇인가요? 다른 이벤트 타입은 왜 안 됐나요?

PreUpdate나 PostUpdate 이벤트는 트랜잭션이 커밋되기 전에 발동합니다. 이 시점에 RabbitMQ로 캐시 갱신 메시지를 보내면, 이후 트랜잭션이 롤백되어도 다른 서버 인스턴스들은 이미 갱신 메시지를 받아서 캐시를 갱신합니다. DB에는 변경이 반영되지 않았는데 캐시만 갱신된 상태가 되어 정합성이 깨집니다. PostCommit은 트랜잭션이 실제로 커밋된 이후에만 발동하므로 이 문제를 방지합니다.

Q3. Decorator 패턴을 도메인 로직에 어떻게 적용했나요?

여러 슬롯 타입에 흩어진 계산 로직(베팅 금액 계산 등)을 도메인 레이어로 이동할 때 사용했습니다. 기존에는 static 유틸 메서드로 여러 곳에서 직접 호출하는 방식이었는데, 이렇게 되면 테스트에서 static 메서드를 모킹할 수 없어서 계산 로직이 포함된 서비스를 격리 테스트하기 어렵습니다. Decorator를 스프링 빈으로 만들고 DI로 주입받으면 테스트에서 모킹이 가능해지고, 계산 로직의 책임 범위도 명확해집니다.

Q4. 447개 테스트 파일을 유지하는 데 드는 관리 비용은 어떻게 처리했나요?

AbstractSlotTest 제네릭 추상 클래스가 관리 비용을 크게 줄였습니다. 각 슬롯 테스트가 공통으로 필요한 초기화(릴 테이블 로드, 심볼 설정, Alias 테이블 생성)를 추상 클래스에서 자동 처리하기 때문에, 새 슬롯 테스트를 추가할 때 초기화 코드를 반복 작성하지 않아도 됩니다. static 메서드를 DI로 전환한 것도 테스트 유지에 큰 도움이 됐습니다. 이전에는 static 의존 때문에 테스트가 외부 자원에 의존하는 경우가 있었는데, DI 전환 후 완전한 격리 테스트가 가능해졌습니다.

Q5. 올리브영의 상품·전시·주문 도메인에서 비슷한 구조 문제가 생길 수 있다고 생각하나요?

커머스 도메인도 유사한 패턴이 반복됩니다. 주문 타입(일반 주문, 묶음 주문, 정기구독 주문)이나 상품 타입(일반 상품, 디지털 상품, 한정판)마다 공통 처리 흐름이 있으면서 타입별로 달라지는 동작이 있습니다. AbstractPlayService + SpinOperationHandler 패턴처럼 공통 흐름을 추상 템플릿으로 통합하고, 타입별 동작은 전략 패턴으로 분리하면 새 주문 타입이나 상품 타입이 추가될 때 기존 코드 변경을 최소화할 수 있습니다. 이 구조를 만들어본 경험이 복잡한 커머스 도메인에 바로 적용될 수 있다고 생각합니다.


핵심 질문 5: RCC 시스템 — 비동기 설계와 DB 성능 최적화

예상 질문 형태

"복잡한 상태를 관리하는 시스템을 설계한 경험이 있나요? 비동기 처리와 DB 인덱스 최적화를 실제 문제에서 다뤄본 적 있나요?"

면접관이 실제로 보는 것

  • 단순 기능 구현을 넘어서, 성능과 안정성을 고려한 시스템 설계 경험이 있는가
  • DB 쿼리 성능 문제를 어떻게 발견하고 해결했는가 (EXPLAIN 플랜, 인덱스 설계)
  • 분산 환경의 동시성 제어를 실제 트레이드오프로 결정할 수 있는가

답변에 반드시 포함할 근거 · 수치 · 사례

항목구체적 내용
시스템 목적RTP 편차 문제 → 백그라운드에서 "좋은 결과"를 미리 생성해 DB에 캐시
비동기 설계@Async로 캐시 생성과 스핀 응답 흐름 분리, 유저 경험에 영향 없음
인덱스 문제캐시 개수 판단 쿼리(COUNT)가 풀스캔, 복합 인덱스(game_id, bet_index, cache_type, used) 추가로 해결
동시성 제어낙관적 락 검토 → DB 유니크 키 + 예외 처리 조합 선택 (충돌 빈도 낮음, 충돌 시 재시도 불필요)
추상화RccSpinResultAnalyzer 인터페이스로 슬롯별 캐시 조건 분리, OCP 적용
모니터링RccCacheStatisticsService, log_slot_play에 RCC 컬럼 추가

피해야 할 약한 답변 패턴

  • "비동기 처리로 성능 개선했다"만 말하면 맥락이 없다. 왜 동기로 하면 안 되는지(캐시 생성이 동기라면 스핀 응답 시간에 직접 영향)를 설명해야 한다.
  • 인덱스를 "그냥 추가했다"고 하면 발견 과정이 사라진다. 백그라운드 작업에서 CPU 사용률 이상 → Slow Query Log → EXPLAIN → 풀스캔 확인 → 복합 인덱스 추가라는 흐름을 설명해야 한다.
  • 동시성 제어를 "synchronized 걸면 된다"고 하면 분산 환경 이해 부족으로 보인다. 여러 서버 인스턴스가 동시에 캐시를 생성하는 상황에서 JVM 수준의 락은 의미가 없다.

압축 답변 구조 (STAR)

S(상황): 순수 확률 기반 슬롯은 짧은 세션에서 RTP 편차가 큽니다. 운이 나쁜 유저는 오랫동안 보상을 받지 못해 이탈할 수 있었습니다.

T(문제): 서비스 응답 흐름에 영향 없이 RTP를 보장하는 캐시 시스템이 필요했고, 여러 서버 인스턴스의 동시 캐시 생성과 성능 문제도 해결해야 했습니다.

A(해결): @Async로 백그라운드 캐시 생성과 스핀 응답 흐름을 분리했습니다. 동시성은 낙관적 락 대신 DB 유니크 키 + 예외 처리로 단순하게 해결했습니다. 캐시 개수 판단 쿼리가 풀스캔하고 있다는 것을 발견하고 복합 인덱스를 추가해 성능 문제를 해결했습니다.

R(결과): 스핀 응답 시간에 영향 없이 RTP 보장 구조를 갖췄고, 백그라운드 쿼리 성능을 개선해 불필요한 CPU 사용을 줄였습니다.

꼬리 질문 5개

Q1. 낙관적 락 대신 DB 유니크 키 + 예외 처리를 선택한 이유는 무엇인가요?

낙관적 락은 충돌 감지 후 재시도 로직이 필요합니다. 그런데 캐시 생성의 경우, 충돌이 발생했다는 것은 다른 인스턴스가 이미 같은 캐시를 생성 중이라는 의미입니다. 충돌 시 재시도 자체가 의미가 없습니다. DB 유니크 키로 중복 생성을 막고, 위반 예외를 "이미 생성됨"으로 처리하는 방식이 더 단순하고 충분했습니다. 충돌 빈도가 낮은 케이스에서는 낙관적 락의 재시도 복잡도가 오히려 과도한 설계가 됩니다.

Q2. 캐시 개수 판단 쿼리의 인덱스 누락을 어떻게 발견했나요?

캐시 생성 로직이 백그라운드에서 주기적으로 실행되는데, 서버 CPU 사용률이 예상보다 높은 게 이상했습니다. Slow Query Log를 확인하니 COUNT(*) 쿼리가 잡혔습니다. EXPLAIN으로 실행 계획을 보니 game_id, bet_index, cache_type, used 복합 조건이지만 인덱스를 타지 않고 풀스캔이었습니다. 복합 인덱스를 추가하고 나서 실행 계획에 index 타입이 잡히는 것을 확인했습니다. 캐시 생성은 백그라운드에서 계속 돌기 때문에 이 쿼리가 느리면 지속적으로 CPU를 잡아먹습니다.

Q3. 시뮬레이터에서 RCC 캐시가 쌓이는 문제를 어떻게 발견하고 해결했나요?

시뮬레이터로 수만 번 스핀을 돌린 후 DB를 확인했더니 대량의 RCC 캐시 레코드가 생성되어 있었습니다. 시뮬레이터는 실제 유저 요청과 동일한 서비스 레이어를 태우기 때문에 RccHandler도 실행된 것입니다. 시뮬레이터 실행 여부를 나타내는 컨텍스트 플래그를 추가하고, RccHandler 진입 시 이 플래그를 체크해 시뮬레이터 실행 중에는 캐시 생성 로직을 스킵하도록 했습니다.

Q4. RccSpinResultAnalyzer 인터페이스 추상화가 실제로 어떻게 개발 속도를 높였나요?

슬롯마다 "어떤 결과를 좋은 결과로 볼 것인가"의 기준이 달랐습니다. 처음부터 인터페이스로 추상화했기 때문에 새 슬롯에 RCC를 붙일 때 RccCacheGenerator 코드를 건드리지 않고 Analyzer 구현체만 만들었습니다. 총 6종 슬롯에 RCC를 붙였는데, 공통 코드 변경 없이 슬롯별 구현체만 추가하는 방식으로 진행됐습니다. 처음에 추상화하지 않았다면 슬롯마다 조건 분기가 쌓여서 나중에 수정하기 어려운 코드가 됐을 것입니다.

Q5. 이 구조를 올리브영의 커머스 컨텍스트에 비유한다면 어떤 케이스에 적용할 수 있을까요?

비슷한 패턴이 적용될 수 있는 케이스로 미리 생성해두는 추천 결과나 개인화 콘텐츠가 있습니다. 올영세일 같은 피크 트래픽 시간에는 실시간으로 추천 결과를 계산하기보다 미리 생성해둔 결과를 캐시에서 꺼내 제공하면 응답 속도를 보장할 수 있습니다. 백그라운드 생성(@Async), 타입별 생성 조건 추상화(Strategy Pattern), 생성 여부 모니터링이라는 구조 자체는 그대로 적용 가능합니다. 올리브영이 캐시 소진 시 DB에서 직접 서빙으로 graceful degradation 하는 패턴도 RCC와 동일한 구조입니다.


공통 답변 원칙 체크리스트

답변하기 전에 이 체크리스트를 머릿속으로 확인한다.

구조 원칙

  • 상황 → 문제 → 해결 → 결과 순으로 말하는가
  • 해결책을 먼저 말하고 "왜"는 나중에 말하는 실수를 하고 있지 않은가
  • 수치나 구체적 사례 없이 추상적으로 설명하고 있지 않은가
  • 답변이 1분 30초를 넘기지 않는가 (핵심만, 꼬리 질문에서 더 파고들 것)

올리브영 맥락 연결

  • "이 경험이 올리브영 커머스 환경에서 어떻게 쓰일 수 있는가"를 한 문장으로 연결할 수 있는가
  • 올리브영 기술 블로그에서 확인한 실제 기술 스택(Cache-Aside, Kafka, Resilience4j)과 내 경험을 연결하고 있는가
  • "1,600만 고객 규모"라는 컨텍스트를 염두에 두고 설계를 설명하고 있는가

기술 깊이 원칙

  • 도구(Kafka, StampedLock, AsyncItemProcessor)를 사용한 이유를 설명하고 있는가
  • 트레이드오프를 의식하고 말하는가 (왜 다른 방법 말고 이 방법인가)
  • 실패한 경험이나 수정한 경험을 포함하고 있는가 (시뮬레이터 캐시 문제, 인덱스 누락 등)
  • JPA/Hibernate 관련 질문에서 ORM 추상화 아래 실제로 어떤 쿼리가 나가는지 설명할 수 있는가

피해야 할 패턴

  • "잘 모르겠지만..." 으로 시작하지 않는다. 모르면 "그 부분은 제가 직접 다루지 않았는데, 제가 했던 유사한 케이스를 말씀드리면..."으로 전환한다
  • 기술 용어만 나열하지 않는다. 면접관이 왜 물어보는지를 의식하고 답한다
  • 팀 공동 작업을 과도하게 강조하지 않는다. "팀에서 했다"보다 "내가 설계하고 구현했다"는 주체를 분명히 한다

마지막 확인: 날카로운 꼬리 질문 대비 목록

면접관이 어떤 방향으로도 파고들 수 있는 꼬리 질문들을 미리 준비한다.

주제예상 날카로운 질문핵심 답변 방향
캐시"캐시 히트율은 얼마였나요?"정적 데이터라 히트율 매우 높음, 갱신 빈도가 낮아 TTL 없이 이벤트 기반 갱신
Kafka"Consumer lag이 쌓이면 어떻게 되나요?"후처리 로직이므로 약간의 지연은 허용, 모니터링으로 임계치 감지, Dead Letter Topic
Spring Batch"배치 중 OOM이 나면 어떻게 하나요?"청크 단위 처리로 전체를 메모리에 올리지 않음, 청크 사이즈 조정, 첨부파일은 바이트 배열 대신 스트림
JPA"N+1 문제를 경험한 적 있나요?"슬롯 설정 조회 시 게임별 N+1 발생 → IN절로 일괄 조회로 해결 (Alias 테이블 조회 최적화)
동시성"StampedLock이 데드락 날 수 있지 않나요?"StampedLock은 재진입 미지원이라 중첩 락 주의, 락 범위 최소화 원칙 준수
설계"추상화를 너무 일찍 한 것 아닌가요?"슬롯 5종 이상 쌓인 후에 공통점이 보여서 추상화, 조기 추상화 함정 피하는 원칙으로 답변
interview 카테고리의 다른 글 보기수정 제안하기

댓글

댓글을 불러오는 중...
  • [초안] CJ 올리브영 면접 대비: 실전 경험 기반 질문 답변 준비 시트
  • 내 경험 내러티브: 3분 자기소개 뼈대
  • 핵심 질문 1: 다중 서버 인메모리 캐시 정합성
  • 예상 질문 형태
  • 면접관이 실제로 보는 것
  • 답변에 반드시 포함할 근거 · 수치 · 사례
  • 피해야 할 약한 답변 패턴
  • 압축 답변 구조 (STAR)
  • 꼬리 질문 5개
  • 핵심 질문 2: Kafka Transactional Outbox Pattern
  • 예상 질문 형태
  • 면접관이 실제로 보는 것
  • 답변에 반드시 포함할 근거 · 수치 · 사례
  • 피해야 할 약한 답변 패턴
  • 압축 답변 구조 (STAR)
  • 꼬리 질문 5개
  • 핵심 질문 3: Spring Batch 대용량 파이프라인 설계
  • 예상 질문 형태
  • 면접관이 실제로 보는 것
  • 답변에 반드시 포함할 근거 · 수치 · 사례
  • 피해야 할 약한 답변 패턴
  • 압축 답변 구조 (STAR)
  • 꼬리 질문 5개
  • 핵심 질문 4: 도메인 로직 추상화 및 슬롯 엔진 구조 개선
  • 예상 질문 형태
  • 면접관이 실제로 보는 것
  • 답변에 반드시 포함할 근거 · 수치 · 사례
  • 피해야 할 약한 답변 패턴
  • 압축 답변 구조 (STAR)
  • 꼬리 질문 5개
  • 핵심 질문 5: RCC 시스템 — 비동기 설계와 DB 성능 최적화
  • 예상 질문 형태
  • 면접관이 실제로 보는 것
  • 답변에 반드시 포함할 근거 · 수치 · 사례
  • 피해야 할 약한 답변 패턴
  • 압축 답변 구조 (STAR)
  • 꼬리 질문 5개
  • 공통 답변 원칙 체크리스트
  • 구조 원칙
  • 올리브영 맥락 연결
  • 기술 깊이 원칙
  • 피해야 할 패턴
  • 마지막 확인: 날카로운 꼬리 질문 대비 목록