Java 리드·아키텍트 종합 기술면접
새 면접Q. 일 100억 건의 이벤트를 처리하는 실시간 분석 플랫폼을 설계해야 합니다. Kafka를 중심으로 한 이벤트 기반 아키텍처에서 백프레셔(backpressure) 제어, 메시지 순서 보장, 정확히 한 번(exactly-once) 처리를 모두 만족시키는 설계를 제시해주세요. 특히 컨슈머 그룹 리밸런싱 시 데이터 유실 방지 전략과 장애 복구 시나리오를 포함해 설명해주세요.
트랜잭셔널 프로듀서/컨슈머, 파티션 전략, 오프셋 커밋 타이밍을 중심으로 생각해보세요.
Kafka 트랜잭셔널 API를 활용하여 프로듀서-컨슈머 간 exactly-once 시맨틱을 구현하고, 파티션 키 설계로 순서 보장이 필요한 이벤트를 동일 파티션에 배치합니다. 백프레셔는 Reactive Streams(Project Reactor) 기반으로 컨슈머의 처리 속도에 따라 폴링 간격과 배치 사이즈를 동적 조절하며, max.poll.records와 fetch.max.wait.ms를 튜닝합니다. 리밸런싱 시에는 ConsumerRebalanceListener를 구현하여 오프셋을 외부 저장소(Redis/DB)에 원자적으로 커밋하고, 재시작 시 이를 기반으로 복구합니다. 장애 복구를 위해 Dead Letter Queue 패턴과 Circuit Breaker를 적용하여 독성 메시지로 인한 전체 파이프라인 중단을 방지하고, 모니터링으로 컨슈머 랙과 처리 지연을 추적합니다. 파티션 수는 최대 동시 처리 스레드 수를 고려하여 설계하고, 수평 확장 시 리밸런싱 비용을 최소화하도록 적절한 파티션 수를 사전 할당합니다.
- • Kafka 트랜잭셔널 API로 exactly-once 보장
- • Reactive Streams 기반 백프레셔 제어
- • ConsumerRebalanceListener로 오프셋 안전 관리
- • DLQ와 Circuit Breaker로 장애 격리
- • 파티션 설계와 컨슈머 랙 모니터링
대규모 로그 수집, 실시간 추천 시스템, 금융 거래 처리 등에서 데이터 유실 없이 높은 처리량을 보장해야 할 때 사용됩니다.
파티션 수를 증가시킬 때 기존 메시지의 순서 보장이 깨질 수 있는데, 무중단으로 파티션을 확장하는 전략은 무엇인가요?
Q. Java의 메모리 모델(JMM)에서 happens-before 관계와 메모리 가시성 문제를 설명하고, volatile, synchronized, final 키워드가 각각 어떻게 이 문제를 해결하는지 비교해주세요. 특히 최신 멀티코어 CPU의 캐시 일관성 프로토콜(MESI)과의 연관성을 포함하여 설명해주세요.
CPU 캐시, 메모리 배리어, 명령어 재배치 관점에서 각 키워드의 역할을 생각해보세요.
JMM의 happens-before 관계는 멀티스레드 환경에서 한 스레드의 쓰기 작업이 다른 스레드의 읽기 작업에 보이는 순서를 정의하며, 이를 통해 메모리 가시성을 보장합니다. volatile은 변수 읽기/쓰기 시 메모리 배리어를 삽입하여 CPU 캐시를 우회하고 메인 메모리에 직접 접근하게 하며, MESI 프로토콜에서 Invalid 상태로 만들어 다른 코어의 캐시를 무효화합니다. synchronized는 모니터 락을 통해 임계 영역 진입/탈출 시 메모리 배리어를 설정하고, 락 획득 전 모든 공유 변수를 메인 메모리에서 읽고 락 해제 시 쓰기를 플러시합니다. final 필드는 생성자 완료 시점에 happens-before 관계를 형성하여 불변 객체의 안전한 발행을 보장하며, 초기화 후 값 변경이 없으므로 추가 동기화가 불필요합니다. 성능 측면에서 volatile은 원자성을 보장하지 않아 단순 플래그에 적합하고, synchronized는 배타적 접근으로 경합이 발생하며, final은 런타임 오버헤드가 없어 가장 효율적입니다.
- • happens-before 관계로 메모리 가시성 정의
- • volatile은 메모리 배리어로 캐시 우회
- • synchronized는 모니터 락으로 임계 영역 보호
- • final은 생성자 완료 시 안전한 발행 보장
- • MESI 프로토콜과 캐시 일관성 연관
싱글톤 패턴 구현, 멀티스레드 캐시 관리, 상태 플래그 제어 등에서 올바른 동기화 메커니즘 선택이 필요합니다.
Double-Checked Locking 패턴에서 volatile을 사용하지 않으면 왜 문제가 발생하며, 이를 JVM 명령어 재배치 관점에서 설명해주세요.
Q. 수천만 건의 주문 데이터를 가진 테이블에서 월별 집계 쿼리 성능이 지속적으로 저하되고 있습니다. 파티셔닝, 머티리얼라이즈드 뷰, 인덱스 전략을 조합한 최적화 방안을 제시하고, 각 방법의 트레이드오프와 운영 복잡도를 비교해주세요. 특히 실시간 데이터 입력과 배치 집계가 동시에 일어나는 상황에서의 락 경합 최소화 전략도 포함해주세요.
시계열 데이터 특성, 읽기/쓰기 패턴 분리, 점진적 집계 방식을 고려해보세요.
주문 날짜 기준 Range 파티셔닝을 적용하여 월별 파티션을 생성하고, 과거 데이터는 읽기 전용으로 전환하여 파티션 프루닝으로 스캔 범위를 최소화합니다. 머티리얼라이즈드 뷰는 일/월 단위 사전 집계 결과를 저장하되, Incremental Refresh 방식으로 변경된 파티션만 갱신하여 전체 재계산 비용을 줄입니다. 복합 인덱스는 (order_date, status, customer_id) 순으로 구성하여 커버링 인덱스로 테이블 접근을 회피하고, 파티션 로컬 인덱스로 각 파티션 내 검색을 최적화합니다. 실시간 입력은 최신 파티션에만 발생하므로 과거 파티션의 집계 쿼리와 락 경합이 없으며, 배치 집계 시에는 MVCC를 활용하여 스냅샷 격리 수준에서 읽기 일관성을 보장합니다. 운영 측면에서 파티션 자동 생성 스크립트와 오래된 파티션 아카이빙 정책을 수립하고, 머티리얼라이즈드 뷰 갱신 실패 시 알림 체계를 구축합니다.
- • Range 파티셔닝으로 스캔 범위 축소
- • Incremental Refresh 머티리얼라이즈드 뷰
- • 커버링 인덱스와 파티션 로컬 인덱스
- • MVCC로 읽기/쓰기 락 경합 최소화
- • 파티션 생명주기 관리 자동화
전자상거래 주문 분석, 로그 집계, 금융 거래 리포팅 등 대용량 시계열 데이터 처리에서 필수적입니다.
파티션 키를 변경해야 하는 상황이 발생했을 때, 무중단으로 리파티셔닝하는 전략은 무엇인가요?
Q. 기술 부채가 심각한 레거시 시스템을 리팩토링하려 했으나, 비즈니스 팀은 신규 기능 개발을 우선시하며 일정을 압박했습니다. 이런 상황에서 기술 리더로서 어떻게 우선순위를 조율하고 이해관계자를 설득했는지 경험을 공유해주세요. 특히 기술 부채의 비즈니스 임팩트를 정량화한 방법과 점진적 개선 전략을 중심으로 설명해주세요.
기술 부채를 비즈니스 언어로 번역하고, 작은 성공을 통해 신뢰를 쌓는 접근을 생각해보세요.
모범 답변은 STAR 형식(Situation-Task-Action-Result)으로 구성되어야 합니다. Situation에서는 레거시 시스템의 구체적 문제(배포 시간, 장애 빈도, 개발 속도 저하)를 정량적으로 제시합니다. Task에서는 기술 부채 해결과 신규 기능 개발의 균형을 맞추는 목표를 설정합니다. Action에서는 장애 비용, 개발 생산성 손실을 월간 금액으로 환산하여 경영진에게 보고하고, Strangler Fig 패턴으로 신규 기능 개발 시 새로운 아키텍처를 점진적으로 적용하는 하이브리드 전략을 제안합니다. 2주 스프린트마다 20% 시간을 리팩토링에 할당하는 규칙을 합의하고, 리팩토링 후 배포 시간 단축, 버그 감소율을 대시보드로 공유하여 가시적 성과를 입증합니다. Result에서는 6개월 후 배포 시간 50% 단축, 장애 30% 감소 등 구체적 지표로 성과를 제시하고, 이를 통해 비즈니스 팀의 신뢰를 얻어 추가 리팩토링 시간을 확보한 경험을 공유합니다.
- • 기술 부채를 비용으로 정량화하여 비즈니스 언어로 소통
- • Strangler Fig 패턴으로 점진적 전환
- • 스프린트별 일정 비율 리팩토링 시간 확보
- • 대시보드로 개선 효과를 가시화
- • 작은 성공으로 신뢰를 쌓고 장기 투자 확보
레거시 전환 프로젝트에서 비즈니스와 기술의 우선순위 충돌은 필연적이며, 리더의 조율 능력이 프로젝트 성패를 좌우합니다.
만약 경영진이 여전히 리팩토링 시간을 주지 않는다면, 어떤 대안 전략을 사용하시겠습니까?
Q. Spring WebFlux를 도입하여 기존 Spring MVC 기반 API를 논블로킹 방식으로 전환하려 합니다. Reactor 기반 리액티브 스트림에서 발생할 수 있는 메모리 누수, 스레드 블로킹, 에러 전파 문제를 진단하고 해결하는 방법을 설명해주세요. 특히 JDBC 등 블로킹 라이브러리와의 통합 시 주의사항과 성능 모니터링 전략을 포함해주세요.
Scheduler 격리, 백프레셔, 에러 핸들러 체인, 블로킹 호출 감지를 중심으로 생각해보세요.
Reactor의 Flux/Mono 체인에서 블로킹 호출(JDBC, 동기 HTTP)은 반드시 별도 Scheduler(boundedElastic)로 격리하여 이벤트 루프 스레드를 블로킹하지 않도록 하고, BlockHound를 테스트에 적용하여 블로킹 호출을 자동 감지합니다. 메모리 누수는 무한 스트림이나 구독 해제 누락으로 발생하므로, timeout 연산자로 장시간 대기를 방지하고 Disposable을 명시적으로 dispose하며, onErrorResume/onErrorContinue로 에러 시 스트림을 안전하게 종료합니다. 백프레셔는 limitRate 연산자로 다운스트림 요청 개수를 제한하고, buffer/window 연산자 사용 시 메모리 한계를 설정합니다. 에러 전파는 onErrorMap으로 예외를 변환하고, 전역 에러 핸들러(WebExceptionHandler)로 일관된 응답을 보장하며, Hooks.onOperatorDebug로 스택트레이스를 강화합니다. 모니터링은 Micrometer와 통합하여 리액티브 스트림의 구독/요청/취소 메트릭을 수집하고, Spring Boot Actuator로 이벤트 루프 스레드 상태와 메모리 사용량을 추적합니다.
- • 블로킹 호출을 boundedElastic Scheduler로 격리
- • BlockHound로 블로킹 감지 자동화
- • timeout과 Disposable로 메모리 누수 방지
- • limitRate로 백프레셔 제어
- • Micrometer로 리액티브 메트릭 수집
높은 동시성이 요구되는 API 게이트웨이, 실시간 스트리밍 서비스, IoT 데이터 수집 등에서 적은 스레드로 많은 요청을 처리할 때 사용됩니다.
R2DBC를 사용할 때 트랜잭션 관리는 어떻게 다르며, @Transactional이 리액티브 환경에서 어떻게 동작하나요?
Q. 프로덕션 환경에서 Full GC가 빈번하게 발생하여 API 응답 시간이 수 초씩 지연되는 현상이 발생했습니다. Heap Dump 분석, GC 로그 해석, JVM 튜닝 과정을 단계별로 설명하고, Old Generation에 객체가 과도하게 쌓이는 근본 원인을 찾는 방법과 장기적 해결 전략을 제시해주세요.
메모리 누수 패턴, 객체 생명주기, GC 알고리즘 선택, 힙 크기 조정을 종합적으로 고려하세요.
먼저 -Xlog:gc* 옵션으로 GC 로그를 상세히 수집하여 Full GC 빈도, 소요 시간, 회수된 메모리 양을 분석하고, jstat으로 실시간 힙 사용량과 GC 통계를 모니터링합니다. jmap으로 힙 덤프를 생성하고 Eclipse MAT나 VisualVM으로 Dominator Tree를 분석하여 메모리를 많이 점유하는 객체와 GC Root 경로를 추적합니다. 일반적으로 캐시 무제한 증가, ThreadLocal 미정리, 리스너 미해제가 주요 원인이며, Leak Suspects 리포트로 의심 지점을 식별합니다. 단기적으로는 Old Generation 크기를 늘리고(-Xmx, -XX:NewRatio 조정) G1GC나 ZGC로 전환하여 Pause Time을 단축하며, -XX:MaxGCPauseMillis로 목표 지연 시간을 설정합니다. 장기적으로는 코드 레벨에서 대용량 컬렉션을 스트림 처리로 전환하고, 캐시에 TTL과 크기 제한(Caffeine, Guava)을 적용하며, WeakReference/SoftReference를 활용하여 메모리 압박 시 자동 회수되도록 합니다. 모니터링 대시보드에 GC 메트릭을 추가하여 재발을 조기 감지합니다.
- • GC 로그와 jstat으로 패턴 분석
- • 힙 덤프를 MAT로 분석하여 누수 지점 식별
- • G1GC/ZGC로 전환하여 Pause Time 단축
- • 캐시에 TTL과 크기 제한 적용
- • Reference 객체로 메모리 압박 대응
대규모 트래픽 서비스에서 GC로 인한 응답 지연은 사용자 경험을 직접 저하시키므로, JVM 튜닝은 성능 최적화의 핵심입니다.
G1GC의 Region 기반 구조가 어떻게 Full GC를 줄이는지, Mixed GC와의 차이를 설명해주세요.
Q. 대규모 배치 작업에서 수백만 건의 데이터를 처리할 때 Spring Batch를 사용합니다. Chunk 기반 처리에서 메모리 효율성, 트랜잭션 범위, 재시작 가능성, 병렬 처리를 모두 고려한 최적화 전략을 설계해주세요. 특히 실패한 아이템만 재처리하는 Skip/Retry 정책과 파티셔닝 전략을 포함해 설명해주세요.
Chunk 크기, ItemReader 커서/페이징, 파티션 스텝, ExecutionContext 활용을 생각해보세요.
Chunk 크기는 메모리와 트랜잭션 커밋 빈도의 균형을 고려하여 1000~5000 정도로 설정하고, JpaPagingItemReader나 JdbcCursorItemReader를 사용하여 전체 데이터를 메모리에 로드하지 않고 스트리밍 방식으로 읽습니다. 트랜잭션은 Chunk 단위로 커밋하여 실패 시 해당 청크만 롤백하고, ExecutionContext에 마지막 처리 위치를 저장하여 재시작 시 중복 처리를 방지합니다. Skip 정책은 SkipPolicy를 구현하여 특정 예외(데이터 포맷 오류)는 건너뛰고 SkipListener로 실패 아이템을 별도 테이블에 기록하며, Retry 정책은 일시적 오류(네트워크 타임아웃)에 대해 지수 백오프로 재시도합니다. 병렬 처리는 Partitioning Step으로 데이터를 범위별로 분할하고 각 파티션을 별도 스레드나 원격 워커에서 독립 실행하며, PartitionHandler로 분산 환경을 구성합니다. 모니터링은 JobRepository의 메타데이터를 조회하여 각 스텝의 처리 건수, 실패율, 소요 시간을 추적하고, 병목 구간을 식별하여 Reader/Processor/Writer를 개별 최적화합니다.
- • Chunk 크기를 메모리-트랜잭션 균형으로 조정
- • Paging/Cursor ItemReader로 스트리밍 처리
- • Skip/Retry 정책으로 탄력적 오류 처리
- • Partitioning으로 병렬 처리 구현
- • ExecutionContext로 재시작 가능성 보장
대용량 정산, 데이터 마이그레이션, 일일 집계 작업 등에서 안정적이고 효율적인 배치 처리가 필수적입니다.
멀티 스레드 환경에서 ItemReader의 Thread-safety를 어떻게 보장하며, 동시성 문제를 해결하는 방법은 무엇인가요?
Q. 마이크로서비스 아키텍처에서 서비스 간 의존성이 많은 환경의 테스트 전략을 수립하려 합니다. 단위 테스트, 통합 테스트, 계약 테스트(Contract Test), E2E 테스트의 비율과 범위를 어떻게 설계하시겠습니까? 특히 Pact나 Spring Cloud Contract를 활용한 CDC(Consumer-Driven Contract) 테스트 도입 전략과 테스트 환경 구성(Testcontainers, WireMock)을 포함해 설명해주세요.
테스트 피라미드, 격리 수준, 빠른 피드백, 프로덕션 유사성의 트레이드오프를 고려하세요.
테스트 피라미드에 따라 단위 테스트 70%, 통합 테스트 20%, E2E 테스트 10% 비율로 구성하여 빠른 피드백과 실행 속도를 우선시합니다. 단위 테스트는 Mockito로 외부 의존성을 모킹하고 비즈니스 로직을 격리하여 검증하며, 통합 테스트는 @SpringBootTest와 Testcontainers로 실제 DB/Redis/Kafka 컨테이너를 띄워 인프라 통합을 검증합니다. CDC 테스트는 Spring Cloud Contract로 프로바이더가 스텁을 생성하고 컨슈머가 이를 기반으로 테스트하여, API 변경 시 컨슈머 호환성을 자동 검증하며 배포 전 계약 위반을 조기 발견합니다. WireMock은 외부 API를 모킹하여 네트워크 의존성 없이 다양한 응답 시나리오(타임아웃, 오류)를 재현하고, E2E 테스트는 주요 사용자 시나리오만 선별하여 스테이징 환경에서 실행합니다. CI/CD 파이프라인에서 단위/통합 테스트는 매 커밋마다, CDC 테스트는 일일 빌드, E2E는 배포 직전에 실행하여 피드백 속도를 최적화하고, 테스트 커버리지는 JaCoCo로 80% 이상 유지하되 핵심 비즈니스 로직은 100%를 목표로 합니다.
- • 테스트 피라미드로 70-20-10 비율 구성
- • Testcontainers로 실제 인프라 통합 테스트
- • Spring Cloud Contract로 CDC 테스트 구현
- • WireMock으로 외부 의존성 모킹
- • CI/CD 단계별 테스트 전략 차별화
마이크로서비스에서 서비스 간 인터페이스 변경으로 인한 장애를 사전 방지하고, 안전한 배포를 보장하는 데 필수적입니다.
CDC 테스트에서 프로바이더가 계약을 위반했을 때, 배포를 자동으로 차단하는 파이프라인을 어떻게 구성하시겠습니까?
Q. Kubernetes 환경에서 Java 애플리케이션의 무중단 배포를 구현하려 합니다. Rolling Update 중 발생할 수 있는 문제(연결 끊김, 요청 유실, 긴 종료 시간)를 해결하기 위한 Graceful Shutdown, Readiness/Liveness Probe, PreStop Hook 설정 전략을 설명해주세요. 특히 JVM의 긴 시작 시간과 Spring Boot의 초기화 과정을 고려한 최적화 방안도 포함해주세요.
Pod 라이프사이클, SIGTERM 처리, 커넥션 드레이닝, 헬스체크 타이밍을 중심으로 생각하세요.
Graceful Shutdown은 server.shutdown=graceful과 spring.lifecycle.timeout-per-shutdown-phase를 설정하여 SIGTERM 수신 시 새 요청을 거부하고 진행 중인 요청이 완료될 때까지 대기하며, PreStop Hook에서 sleep 10을 추가하여 로드밸런서가 엔드포인트를 제거할 시간을 확보합니다. Readiness Probe는 /actuator/health/readiness로 애플리케이션 초기화 완료를 확인하고 initialDelaySeconds를 JVM 워밍업 시간(30초)보다 길게 설정하여 준비되지 않은 Pod로 트래픽이 유입되지 않도록 하며, Liveness Probe는 /actuator/health/liveness로 데드락이나 OOM을 감지하여 자동 재시작합니다. JVM 시작 시간 단축을 위해 AppCDS(Application Class-Data Sharing)로 클래스 메타데이터를 사전 로드하고, -XX:TieredStopAtLevel=1로 C1 컴파일러만 사용하여 초기 시작 속도를 높이며, Spring Context Indexer로 컴포넌트 스캔을 최적화합니다. Rolling Update는 maxUnavailable=0, maxSurge=1로 설정하여 항상 충분한 Pod가 유지되도록 하고, PodDisruptionBudget으로 최소 가용 Pod 수를 보장합니다. 모니터링은 Prometheus로 Pod 재시작, 요청 실패율, 응답 시간을 추적하여 배포 품질을 검증합니다.
- • Graceful Shutdown과 PreStop Hook으로 요청 완료 보장
- • Readiness Probe로 초기화 완료 후 트래픽 수신
- • AppCDS와 Tiered Compilation으로 JVM 시작 최적화
- • Rolling Update 파라미터로 가용성 유지
- • PodDisruptionBudget으로 최소 Pod 보장
프로덕션 배포 시 사용자 경험을 해치지 않고 새 버전을 안전하게 릴리스하는 것은 SRE의 핵심 과제입니다.
Blue-Green 배포와 Canary 배포를 Kubernetes에서 구현할 때 각각의 장단점과 적합한 시나리오는 무엇인가요?
Q. 대용량 로그 파일에서 상위 K개의 빈출 IP 주소를 찾는 시스템을 설계해야 합니다. 메모리가 제한적인 환경에서 수십 GB의 로그를 처리하기 위한 알고리즘과 자료구조를 선택하고, 분산 처리 방식으로 확장하는 방안을 설명해주세요. 특히 정확도와 근사 알고리즘의 트레이드오프를 고려해주세요.
외부 정렬, Min Heap, Count-Min Sketch, MapReduce 패턴을 생각해보세요.
정확한 결과가 필요하다면 외부 정렬 방식으로 로그를 IP별로 정렬한 후 순차 스캔하며 카운팅하고, Min Heap(크기 K)을 유지하여 상위 K개를 추출하며 시간 복잡도는 O(N log N)입니다. 메모리 효율을 위해 파일을 청크로 나누어 각 청크에서 로컬 Top-K를 구한 후 병합하는 방식을 사용하고, HashMap으로 빈도를 저장하되 크기가 임계치를 넘으면 하위 절반을 제거하여 메모리를 제한합니다. 근사 알고리즘이 허용된다면 Count-Min Sketch로 해시 기반 확률적 카운팅을 수행하여 메모리를 O(K)로 줄이고, Heavy Hitters 알고리즘(Misra-Gries)으로 빈출 항목을 추적합니다. 분산 처리는 MapReduce 패턴으로 Map 단계에서 각 노드가 로컬 빈도를 집계하고, Reduce 단계에서 IP별 전역 빈도를 합산한 후 최종 Top-K를 선택하며, Combiner로 중간 데이터를 사전 집계하여 네트워크 비용을 절감합니다. Java에서는 Stream API로 병렬 처리하거나 Fork/Join 프레임워크를 활용하고, 대규모 환경에서는 Apache Spark의 reduceByKey와 top 연산을 사용합니다.
- • 외부 정렬과 Min Heap으로 정확한 Top-K 추출
- • Count-Min Sketch로 메모리 효율적 근사 카운팅
- • MapReduce 패턴으로 분산 집계
- • Combiner로 중간 데이터 사전 집계
- • 정확도-메모리-속도 트레이드오프 고려
웹 서버 로그 분석, DDoS 공격 탐지, 추천 시스템의 인기 아이템 추출 등에서 대용량 데이터의 빈도 분석이 필요합니다.
실시간 스트리밍 환경에서 슬라이딩 윈도우 기반 Top-K를 유지하려면 어떤 자료구조와 알고리즘을 사용해야 하나요?
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!