Java 시니어 성능 최적화 기술면접
새 면접Q. 대규모 배치 처리 애플리케이션에서 Old Generation이 급격히 증가하여 Full GC가 빈번하게 발생하고 있습니다. 힙 덤프 분석 결과 대량의 작은 객체들이 Old 영역에 누적되어 있는 상황입니다. Object Allocation Rate와 Promotion Rate를 측정하고 분석하는 방법을 설명하고, Young Generation 크기 조정, Survivor Space 비율 튜닝, 그리고 객체 생성 패턴 개선을 통해 이 문제를 해결하는 전략을 제시해주세요.
GC 로그에서 Allocation Rate와 Promotion Rate를 계산하는 방법부터 생각해보세요.
먼저 GC 로그를 통해 단위 시간당 Young Generation에 할당되는 객체량(Allocation Rate)과 Old Generation으로 승격되는 객체량(Promotion Rate)을 측정합니다. Promotion Rate가 높다면 Young Generation 크기를 늘려 Minor GC 빈도를 줄이고, Survivor Space에서 충분한 aging이 이루어지도록 -XX:SurvivorRatio와 -XX:MaxTenuringThreshold를 조정합니다. 애플리케이션 레벨에서는 StringBuilder 대신 String 연결, 불필요한 임시 객체 생성, Boxing/Unboxing을 줄이고 객체 풀링이나 Flyweight 패턴을 적용합니다. JFR(Java Flight Recorder)이나 VisualVM으로 Allocation Hotspot을 찾아 코드를 개선하며, 배치 처리의 경우 청크 단위로 명시적으로 컬렉션을 clear하여 메모리를 해제합니다. 최종적으로 -Xmn 옵션으로 Young Generation 크기를 전체 힙의 30-40%로 설정하고 GC 로그를 모니터링하며 튜닝 효과를 검증합니다.
- • Allocation Rate와 Promotion Rate 측정 및 분석
- • Young Generation과 Survivor Space 비율 튜닝
- • 객체 생성 패턴 개선 및 Allocation Hotspot 제거
- • GC 로그 기반 성능 검증
대용량 데이터 처리 배치 시스템에서 Full GC로 인한 STW를 줄여 처리 시간을 단축할 때 사용합니다.
G1 GC에서 Humongous Object가 성능에 미치는 영향과 이를 최적화하는 방법은 무엇인가요?
Q. 복잡한 조인과 서브쿼리가 포함된 SQL이 수십 초씩 걸리고 있습니다. Execution Plan을 분석한 결과 Table Scan과 Nested Loop Join이 발견되었습니다. Index Scan, Index Seek, Index-Only Scan의 차이점을 설명하고, Covering Index 설계 전략, Join Order 최적화, 그리고 서브쿼리를 Join이나 Window Function으로 변환하는 리팩토링 기법을 설명해주세요. 또한 통계 정보 갱신과 Query Hint 사용 시 주의사항도 함께 설명해주세요.
실행 계획에서 Cost 값과 Cardinality 추정치를 먼저 확인하세요.
Index Scan은 인덱스 전체를 순회하고, Index Seek은 B-Tree를 통해 특정 범위만 탐색하며, Index-Only Scan은 인덱스만으로 쿼리를 완성합니다. Covering Index는 SELECT와 WHERE 절의 모든 컬럼을 인덱스에 포함시켜 테이블 접근을 제거하며, 인덱스 컬럼 순서는 카디널리티가 높고 WHERE 조건에 자주 사용되는 순서로 배치합니다. Nested Loop Join은 작은 테이블을 outer로, Hash Join은 대용량 테이블 조인에 적합하므로 Join Order를 조정하고, 서브쿼리는 EXISTS를 JOIN으로, Scalar Subquery는 Window Function으로 변환하여 반복 실행을 제거합니다. 통계 정보가 오래되면 옵티마이저가 잘못된 실행 계획을 선택하므로 주기적으로 ANALYZE/UPDATE STATISTICS를 실행하며, Query Hint는 옵티마이저의 자동 최적화를 무시하므로 데이터 분포 변화 시 오히려 성능 저하를 유발할 수 있어 신중하게 사용해야 합니다.
- • Index Scan/Seek/Only Scan의 차이와 Covering Index 설계
- • Join Order 최적화와 Join 알고리즘 선택
- • 서브쿼리를 Join/Window Function으로 변환
- • 통계 정보 관리와 Query Hint 사용 주의
전자상거래 주문 조회 화면에서 복잡한 필터 조건과 집계 쿼리의 응답 시간을 최적화할 때 사용합니다.
Composite Index에서 컬럼 순서를 결정할 때 고려해야 할 요소들은 무엇인가요?
Q. Spring Boot 애플리케이션에서 Tomcat 스레드풀이 고갈되어 요청이 큐에 쌓이고 응답 시간이 급격히 증가하고 있습니다. Thread Dump를 분석한 결과 대부분의 스레드가 WAITING 상태로 외부 API 응답을 기다리고 있습니다. Tomcat의 maxThreads, acceptCount, connectionTimeout 설정의 의미와 적정값 산정 방법을 설명하고, 외부 API 호출을 비동기로 처리하거나 별도의 스레드풀로 격리하는 전략, 그리고 Bulkhead 패턴 적용 방안을 제시해주세요.
스레드풀 크기는 리틀의 법칙(Little's Law)을 활용해 계산할 수 있습니다.
maxThreads는 동시 처리 가능한 최대 요청 수로, 리틀의 법칙(동시 사용자 = 처리율 × 평균 응답시간)을 기반으로 산정하며, acceptCount는 스레드풀 고갈 시 대기 큐 크기로 너무 크면 타임아웃만 지연시키므로 적절히 제한합니다. connectionTimeout은 클라이언트 연결 대기 시간으로 네트워크 환경에 따라 조정합니다. 외부 API 호출은 CompletableFuture와 별도의 ThreadPoolExecutor를 사용하여 비동기로 처리하고, @Async와 커스텀 Executor를 통해 Tomcat 스레드풀과 격리합니다. Resilience4j Bulkhead로 외부 API별 스레드풀을 분리하여 하나의 API 장애가 전체 시스템에 전파되지 않도록 하며, Semaphore 방식을 사용하면 스레드 생성 없이 동시 호출 수만 제한할 수 있습니다. 모니터링은 Micrometer로 스레드풀 사용률과 큐 크기를 추적하고 임계치 도달 시 알림을 설정합니다.
- • 리틀의 법칙 기반 스레드풀 크기 산정
- • 외부 API 호출의 비동기 처리 및 스레드풀 격리
- • Bulkhead 패턴으로 장애 격리
- • 스레드풀 메트릭 모니터링
결제 게이트웨이나 외부 물류 API를 호출하는 주문 처리 시스템에서 스레드풀 고갈을 방지할 때 사용합니다.
Virtual Thread(Project Loom)를 도입하면 이러한 스레드풀 고갈 문제를 어떻게 개선할 수 있나요?
Q. HikariCP를 사용하는 애플리케이션에서 피크 시간대에 Connection timeout 에러가 발생하고 있습니다. maximumPoolSize, minimumIdle, connectionTimeout, idleTimeout, maxLifetime 파라미터의 역할과 상호작용을 설명하고, 데이터베이스의 max_connections 설정과의 관계, 그리고 Connection Leak을 탐지하고 방지하는 방법을 설명해주세요. 또한 멀티 데이터소스 환경에서 커넥션풀을 어떻게 분배해야 하는지도 함께 설명해주세요.
커넥션풀 크기는 CPU 코어 수와 디스크 처리량을 고려해야 합니다.
maximumPoolSize는 풀의 최대 커넥션 수로, HikariCP 권장 공식은 connections = ((core_count × 2) + effective_spindle_count)이며 일반적으로 10-20개가 적정합니다. minimumIdle은 유휴 커넥션 수로 HikariCP는 maximumPoolSize와 동일하게 설정하길 권장합니다. connectionTimeout은 풀에서 커넥션을 얻기까지 대기 시간(기본 30초), idleTimeout은 유휴 커넥션 유지 시간, maxLifetime은 커넥션의 최대 생존 시간으로 DB의 wait_timeout보다 짧게 설정해야 합니다. 전체 애플리케이션 인스턴스의 maximumPoolSize 합이 DB의 max_connections를 초과하지 않도록 설계하며, Connection Leak은 leakDetectionThreshold를 설정하여 탐지하고 try-with-resources나 @Transactional로 확실히 반환되도록 합니다. 멀티 데이터소스 환경에서는 각 데이터소스별 트래픽 비율에 따라 커넥션풀을 분배하고, 읽기 전용 복제본은 별도 풀로 구성하여 쓰기 마스터의 부하를 분산합니다.
- • HikariCP 파라미터의 역할과 권장 설정값
- • DB max_connections와의 관계 및 전체 인스턴스 고려
- • Connection Leak 탐지 및 방지
- • 멀티 데이터소스 환경의 커넥션풀 분배
MSA 환경에서 여러 인스턴스가 동일 DB를 공유할 때 커넥션풀 고갈을 방지하고 최적 성능을 유지할 때 사용합니다.
Read-Write Splitting 환경에서 복제 지연(Replication Lag)을 고려한 라우팅 전략은 무엇인가요?
Q. 재고 차감 로직에서 synchronized 블록으로 인한 Lock Contention이 심각하여 TPS가 낮게 측정되고 있습니다. JProfiler로 분석한 결과 대부분의 스레드가 BLOCKED 상태입니다. Lock Striping, Optimistic Locking, Pessimistic Locking의 차이점과 적용 시나리오를 설명하고, ConcurrentHashMap의 세그먼트 기반 Lock Striping 원리, AtomicLong과 LongAdder의 성능 차이, 그리고 데이터베이스 레벨에서 SELECT FOR UPDATE와 Optimistic Locking(Version 컬럼)의 트레이드오프를 비교해주세요.
Lock의 범위를 줄이거나 Lock-Free 알고리즘을 고려해보세요.
Lock Striping은 하나의 Lock을 여러 개로 분할하여 동시성을 높이는 기법으로, ConcurrentHashMap은 내부적으로 여러 세그먼트로 나누어 각각 독립적인 Lock을 사용합니다. Optimistic Locking은 충돌이 드물 때 적합하며 Version 컬럼으로 수정 전후를 비교하고 충돌 시 재시도하므로 Lock을 잡지 않아 동시성이 높지만 충돌 빈도가 높으면 재시도 오버헤드가 큽니다. Pessimistic Locking(SELECT FOR UPDATE)은 충돌이 빈번할 때 적합하며 확실한 일관성을 보장하지만 Lock 대기로 처리량이 감소합니다. AtomicLong은 모든 스레드가 동일 메모리 위치를 CAS로 경쟁하지만, LongAdder는 여러 Cell로 분산하여 업데이트하고 읽기 시 합산하므로 높은 경합 환경에서 월등히 빠릅니다. 재고 차감은 상품별로 Lock을 분리하거나, Redis의 Lua Script로 원자적 연산을 수행하거나, 메시지 큐로 순차 처리하여 Lock Contention을 제거할 수 있습니다.
- • Lock Striping으로 Lock 범위 축소
- • Optimistic vs Pessimistic Locking 트레이드오프
- • LongAdder의 Cell 분산 구조와 성능 이점
- • 재고 차감의 대안 전략
플래시 세일이나 한정 수량 상품 판매 시 동시 다발적 재고 차감 요청을 안전하고 빠르게 처리할 때 사용합니다.
분산 환경에서 재고 차감의 정합성을 보장하기 위한 분산 락(Redisson, Zookeeper) 구현 방법은 무엇인가요?
Q. 대용량 응답 데이터를 JSON으로 변환하는 과정에서 CPU 사용률이 급증하고 응답 시간이 길어지고 있습니다. Jackson의 ObjectMapper 생성 비용과 재사용 전략, @JsonView를 통한 선택적 직렬화, @JsonIgnore와 Lazy Loading의 조합, 그리고 JsonGenerator를 사용한 스트리밍 방식의 장점을 설명해주세요. 또한 순환 참조 문제를 해결하는 방법과 Jackson의 afterburner 모듈이나 컴파일 타임 직렬화 라이브러리 도입 시 성능 개선 효과를 설명해주세요.
ObjectMapper는 스레드 안전하므로 싱글톤으로 재사용해야 합니다.
ObjectMapper는 리플렉션 기반 직렬화 정보를 캐싱하므로 생성 비용이 크며, 스레드 안전하므로 싱글톤 빈으로 등록하여 재사용해야 합니다. @JsonView를 사용하면 API 엔드포인트별로 필요한 필드만 선택적으로 직렬화하여 데이터 크기와 처리 시간을 줄이고, @JsonIgnore와 JPA Lazy Loading을 조합하면 불필요한 연관 엔티티 조회를 방지합니다. 대용량 데이터는 JsonGenerator로 스트리밍 방식으로 직렬화하면 메모리에 전체 객체 트리를 유지하지 않아 메모리 효율적이며, 순환 참조는 @JsonManagedReference/@JsonBackReference나 @JsonIdentityInfo로 해결합니다. Jackson Afterburner 모듈은 리플렉션 대신 바이트코드 생성으로 20-30% 성능 향상을 제공하며, Gson보다 빠르고 Protobuf나 MessagePack은 바이너리 포맷으로 더 빠르지만 가독성이 떨어지므로 내부 서비스 간 통신에 적합합니다. DTO Projection을 사용해 엔티티 대신 필요한 필드만 조회하는 것도 효과적입니다.
- • ObjectMapper 싱글톤 재사용
- • @JsonView로 선택적 직렬화
- • JsonGenerator 스트리밍 방식
- • Afterburner 모듈과 대안 라이브러리
대용량 상품 목록이나 주문 내역을 API로 제공할 때 직렬화 성능을 최적화하여 응답 시간을 단축할 때 사용합니다.
gRPC와 Protocol Buffers를 도입할 때 JSON 대비 어떤 성능 이점과 트레이드오프가 있나요?
Q. Spring WebFlux 기반 애플리케이션에서 대용량 파일 업로드 처리 시 메모리 사용량이 급증하고 OOM이 발생합니다. Reactive Streams의 백프레셔(Backpressure) 메커니즘을 설명하고, onBackpressureBuffer, onBackpressureDrop, onBackpressureLatest 전략의 차이점과 적용 시나리오를 설명해주세요. 또한 Flux의 limitRate, buffer, window 연산자를 활용한 메모리 제어 방법과, 파일 업로드를 청크 단위로 스트리밍 처리하는 구현 전략을 제시해주세요.
Publisher가 Subscriber의 처리 속도를 고려하여 데이터를 발행하는 메커니즘을 생각해보세요.
Reactive Streams의 백프레셔는 Subscriber가 request(n)으로 처리 가능한 데이터 개수를 Publisher에게 알려 과부하를 방지하는 메커니즘입니다. onBackpressureBuffer는 초과 데이터를 버퍼에 저장하지만 버퍼 오버플로우 위험이 있고, onBackpressureDrop은 초과 데이터를 버리며, onBackpressureLatest는 최신 데이터만 유지하므로 실시간 센서 데이터에 적합합니다. limitRate(n)은 한 번에 요청하는 데이터 개수를 제한하여 메모리 사용을 제어하고, buffer(size)는 일괄 처리를 위해 청크로 묶으며, window(size)는 Flux를 여러 개의 작은 Flux로 분할합니다. 파일 업로드는 DataBufferUtils로 InputStream을 Flux<DataBuffer>로 변환하고 limitRate로 동시 처리 청크 수를 제한하며, flatMap의 concurrency 파라미터로 병렬 처리 수준을 조절합니다. WebClient 호출 시에도 exchangeToFlux로 스트리밍 응답을 처리하여 메모리에 전체 응답을 로드하지 않도록 합니다.
- • 백프레셔의 request(n) 메커니즘
- • onBackpressure 전략별 특성
- • limitRate, buffer, window 연산자 활용
- • 파일 스트리밍 처리 구현
동영상 스트리밍이나 대용량 CSV 파일 업로드 처리 시 메모리 효율적으로 데이터를 처리할 때 사용합니다.
WebFlux에서 Blocking I/O를 호출해야 할 때 어떻게 스레드풀을 분리하고 백프레셔를 적용해야 하나요?
Q. 애플리케이션 재시작 후 초기 요청들의 응답 시간이 매우 느린 Cold Start 문제가 발생하고 있습니다. 캐시가 비어있어 DB 부하가 급증하고 Thundering Herd 현상이 발생합니다. 애플리케이션 시작 시 캐시를 사전에 로딩하는 Cache Warming 전략을 설계할 때, 전체 데이터 vs 핫 데이터 선별 기준, 순차 로딩 vs 병렬 로딩의 트레이드오프, 그리고 초기화 완료 전 헬스체크를 차단하는 방법을 설명해주세요. 또한 Redis Persistence(RDB, AOF)를 활용한 캐시 복구 전략도 함께 설명해주세요.
ApplicationReadyEvent를 활용하여 초기화 로직을 구현할 수 있습니다.
Cache Warming은 애플리케이션 시작 시 자주 조회되는 데이터를 미리 캐시에 로드하여 Cold Start를 방지합니다. 전체 데이터 로딩은 시간이 오래 걸리므로 접근 빈도 로그 분석이나 비즈니스 우선순위로 핫 데이터를 선별하고, 병렬 로딩은 빠르지만 DB 부하를 유발하므로 CompletableFuture로 배치 단위 병렬 처리하되 동시성을 제한합니다. Spring의 ApplicationReadyEvent 리스너에서 초기화를 수행하고, 완료 전까지 HealthIndicator를 DOWN 상태로 유지하여 로드밸런서가 트래픽을 보내지 않도록 합니다. Redis RDB는 특정 시점 스냅샷을 디스크에 저장하여 재시작 시 빠르게 복구하지만 최신 데이터 손실 가능성이 있고, AOF는 모든 쓰기 명령을 로깅하여 데이터 손실을 최소화하지만 복구 시간이 길며, 두 방식을 혼용하는 것이 일반적입니다. Caffeine 같은 로컬 캐시는 Lazy Loading과 Refresh Ahead 전략을 조합하여 백그라운드로 갱신할 수 있습니다.
- • 핫 데이터 선별 및 우선순위 로딩
- • 병렬 로딩의 동시성 제어
- • HealthIndicator로 초기화 완료 제어
- • Redis Persistence 활용
대규모 이벤트나 프로모션 시작 전 애플리케이션 재배포 시 초기 트래픽 폭증에 대비할 때 사용합니다.
Blue-Green 배포 환경에서 새 인스턴스의 캐시를 기존 인스턴스로부터 복제하는 전략은 무엇인가요?
Q. JPA를 사용하여 수백만 건의 데이터를 Insert하는 배치 작업이 너무 느립니다. JPA의 Batch Insert 설정(hibernate.jdbc.batch_size), IDENTITY vs SEQUENCE 전략의 배치 성능 차이, 그리고 영속성 컨텍스트 관리(flush, clear)의 중요성을 설명해주세요. 또한 JDBC Batch와 Spring JdbcTemplate의 batchUpdate, 그리고 DB의 Bulk Insert 문법(INSERT INTO ... VALUES (...), (...))을 비교하고, 각 방식의 성능 특성과 적용 시나리오를 제시해주세요.
영속성 컨텍스트에 엔티티가 계속 쌓이면 메모리 문제가 발생합니다.
JPA Batch Insert는 hibernate.jdbc.batch_size를 설정하여 여러 Insert를 하나의 JDBC Batch로 묶어 네트워크 왕복을 줄입니다. IDENTITY 전략은 Insert 직후 생성된 ID를 조회해야 하므로 배치가 불가능하지만, SEQUENCE나 TABLE 전략은 미리 ID를 할당받아 배치가 가능하므로 대량 Insert에는 SEQUENCE를 사용합니다. 영속성 컨텍스트는 모든 엔티티를 1차 캐시에 보관하므로 주기적으로 flush()로 DB에 반영하고 clear()로 메모리를 비워야 하며, 일반적으로 배치 크기마다 수행합니다. JdbcTemplate.batchUpdate는 JPA보다 오버헤드가 적고 빠르며, PreparedStatement의 addBatch/executeBatch로 직접 구현하면 더욱 세밀한 제어가 가능합니다. DB의 Multi-row Insert(INSERT INTO ... VALUES (...), (...))는 단일 SQL로 여러 행을 삽입하여 가장 빠르지만 SQL 길이 제한과 트랜잭션 크기를 고려해야 합니다. 대용량 Insert는 인덱스를 비활성화하고 작업 후 재생성하거나, DB의 Bulk Load 도구(MySQL LOAD DATA, PostgreSQL COPY)를 사용하는 것이 최선입니다.
- • hibernate.jdbc.batch_size와 SEQUENCE 전략
- • 영속성 컨텍스트 flush/clear 관리
- • JdbcTemplate batchUpdate 활용
- • DB Bulk Load 도구 사용
정산 시스템에서 수백만 건의 거래 내역을 배치로 적재하거나 데이터 마이그레이션 작업 시 사용합니다.
대용량 Update 작업에서 Dirty Checking 오버헤드를 줄이기 위한 방법은 무엇인가요?
Q. MSA 환경에서 서비스 간 REST API 호출이 많아 네트워크 레이턴시가 전체 응답 시간의 대부분을 차지하고 있습니다. HTTP/1.1의 Head-of-Line Blocking 문제와 HTTP/2의 Multiplexing, Server Push가 이를 어떻게 개선하는지 설명하고, gRPC의 HTTP/2 기반 스트리밍과 바이너리 프로토콜의 성능 이점을 설명해주세요. 또한 여러 API를 순차 호출하는 경우 CompletableFuture로 병렬화하는 방법과, GraphQL이나 BFF 패턴으로 여러 호출을 하나로 집약하는 아키텍처 개선 방안을 제시해주세요.
네트워크 왕복 횟수(Round Trip)를 줄이는 것이 핵심입니다.
HTTP/1.1은 하나의 TCP 연결에서 요청을 순차 처리하므로 Head-of-Line Blocking이 발생하고, Keep-Alive로 연결을 재사용해도 동시 요청은 여러 연결을 열어야 합니다. HTTP/2는 하나의 연결에서 여러 스트림을 Multiplexing하여 동시 요청을 처리하고, 헤더 압축(HPACK)과 바이너리 프레임으로 오버헤드를 줄이며, Server Push로 클라이언트 요청 전에 리소스를 전송할 수 있습니다. gRPC는 HTTP/2 기반으로 Protobuf 바이너리 직렬화를 사용하여 JSON 대비 3-5배 빠르고, Bidirectional Streaming으로 실시간 통신이 가능하며, 코드 생성으로 타입 안정성을 보장합니다. 여러 API 호출은 CompletableFuture.allOf로 병렬화하여 총 시간을 최대 레이턴시로 줄이고, GraphQL은 클라이언트가 필요한 데이터를 한 번에 요청하여 Over-fetching과 Under-fetching을 방지하며, BFF 패턴은 프론트엔드별 최적화된 API를 제공하여 불필요한 호출을 제거합니다. Service Mesh의 사이드카 프록시는 연결 풀링과 서킷 브레이커로 네트워크 효율을 높입니다.
- • HTTP/2 Multiplexing과 헤더 압축
- • gRPC의 Protobuf와 스트리밍
- • CompletableFuture 병렬 호출
- • GraphQL/BFF 패턴으로 호출 집약
주문 서비스가 재고, 결제, 배송 서비스를 호출하는 복잡한 MSA 환경에서 전체 응답 시간을 최적화할 때 사용합니다.
Service Mesh(Istio, Linkerd)의 사이드카 프록시가 서비스 간 통신 성능에 미치는 영향은 무엇인가요?
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!