Spring 리드·아키텍트 데이터베이스 기술면접
새 면접Q. 10년간 운영된 Spring 기반 이커머스 플랫폼에서 주문(Order) 테이블이 5억 건을 넘어서면서 조회 성능이 급격히 저하되고 있습니다. 테이블 파티셔닝(Range, Hash, List), 샤딩(Vertical, Horizontal), 그리고 아카이빙 전략 중에서 어떤 방식을 선택하시겠습니까? 각 전략의 데이터 분산 방식, 쿼리 복잡도, 운영 비용, 애플리케이션 레벨 수정 범위를 비교하고, 주문 데이터의 특성(시간 기반 조회 패턴, 고객별 조회, 통계 집계)을 고려한 구체적인 데이터 분할 전략을 제시해주세요.
데이터 접근 패턴과 비즈니스 요구사항에 따라 여러 전략을 조합할 수 있습니다.
주문 데이터는 시간 기반 조회가 주를 이루므로 Range 파티셔닝(월별 또는 분기별)을 우선 적용하고, 1년 이상 경과한 데이터는 별도 아카이브 테이블로 이관하는 하이브리드 전략을 추천합니다. 파티셔닝은 테이블 구조 변경 없이 투명하게 적용 가능하며, 파티션 프루닝으로 쿼리 성능을 개선할 수 있습니다. 샤딩은 단일 DB 한계를 넘는 경우에만 고려하되, 애플리케이션 레벨에서 샤드 키 관리와 크로스 샤드 쿼리 처리가 필요해 복잡도가 높습니다. 통계 집계를 위해서는 별도의 OLAP용 데이터 마트를 구축하고, CDC(Change Data Capture)를 통해 실시간 동기화하는 방식으로 OLTP와 OLAP를 분리합니다. Spring Batch를 활용해 주기적으로 오래된 데이터를 아카이빙하고, JPA의 @Where 애노테이션이나 Hibernate Filter로 활성 데이터만 조회하도록 애플리케이션 레벨에서 제어합니다.
- • 시간 기반 Range 파티셔닝과 아카이빙 하이브리드 전략
- • 파티션 프루닝을 통한 쿼리 성능 개선
- • 샤딩은 복잡도가 높아 단일 DB 한계 도달 시에만 고려
- • OLTP와 OLAP 분리를 위한 데이터 마트 구축
- • Spring Batch 기반 아카이빙 자동화
대용량 트랜잭션 데이터를 다루는 금융, 이커머스 시스템에서 테이블 파티셔닝과 아카이빙은 필수 전략입니다.
파티션 테이블에서 글로벌 인덱스와 로컬 인덱스의 차이는 무엇이며, 각각 어떤 상황에서 사용해야 합니까?
Q. Spring 기반 SaaS 플랫폼에서 다중 테넌트(Multi-tenant) 아키텍처를 구현할 때, tenant_id를 포함한 복합 인덱스 설계 전략을 제시해주세요. 특히 인덱스의 컬럼 순서(tenant_id를 선두에 둘지, 선택도가 높은 컬럼을 앞에 둘지), 커버링 인덱스 적용 가능성, 인덱스 크기 증가에 따른 메모리 사용량, 그리고 대형 테넌트와 소형 테넌트의 데이터 분포 편중(Data Skew) 문제를 어떻게 해결하시겠습니까? MySQL과 PostgreSQL의 인덱스 구조 차이도 고려해주세요.
테넌트별 데이터 격리와 쿼리 성능, 인덱스 선택도를 모두 고려해야 합니다.
다중 테넌트 환경에서는 tenant_id를 복합 인덱스의 선두 컬럼으로 배치하여 테넌트별 데이터 격리와 빠른 필터링을 보장합니다. 이후 컬럼은 쿼리 패턴에 따라 선택도가 높은 순서로 배치하며, WHERE 절에 자주 사용되는 컬럼들을 모두 포함한 커버링 인덱스를 구성해 테이블 액세스를 최소화합니다. MySQL의 InnoDB는 클러스터드 인덱스를 사용하므로 세컨더리 인덱스가 PK를 포함하는 구조이며, PostgreSQL은 힙 테이블 구조로 INCLUDE 절을 활용한 커버링 인덱스 구성이 효과적입니다. 대형 테넌트의 데이터 편중 문제는 파티셔닝으로 물리적 분리를 고려하거나, 대형 테넌트 전용 읽기 복제본을 구성하는 방식으로 해결합니다. 인덱스 크기 모니터링을 위해 pg_stat_user_indexes나 INFORMATION_SCHEMA를 주기적으로 확인하고, 사용되지 않는 인덱스는 제거하여 메모리 효율성을 유지합니다.
- • tenant_id를 복합 인덱스 선두 컬럼으로 배치
- • 커버링 인덱스로 테이블 액세스 최소화
- • MySQL InnoDB와 PostgreSQL의 인덱스 구조 차이 이해
- • 대형 테넌트는 파티셔닝 또는 전용 복제본으로 분리
- • 사용되지 않는 인덱스 주기적 제거
SaaS 플랫폼에서 수천 개의 테넌트 데이터를 효율적으로 관리하기 위한 인덱스 전략은 성능과 비용에 직접적인 영향을 줍니다.
PostgreSQL의 BRIN 인덱스와 B-Tree 인덱스는 어떤 차이가 있으며, 시계열 데이터에서 BRIN이 유리한 이유는 무엇입니까?
Q. Spring 기반 재고 관리 시스템에서 동시에 여러 주문이 같은 상품의 재고를 차감할 때 발생하는 동시성 문제를 해결하기 위한 전략을 비교 분석해주세요. Pessimistic Lock(SELECT FOR UPDATE), Optimistic Lock(Version 컬럼), 분산 락(Redis, Zookeeper), 그리고 데이터베이스 레벨의 Serializable 격리 수준 중에서 각 방식의 동작 원리, 데드락 발생 가능성, 처리량(TPS), 실패 시 재시도 로직, 그리고 멀티 DB 환경에서의 적용 가능성을 설명하고, 실무에서 권장하는 방식을 제시해주세요.
동시성 수준, 실패 처리 복잡도, 성능 요구사항에 따라 적절한 락 전략을 선택해야 합니다.
재고 차감과 같은 높은 동시성 시나리오에서는 Optimistic Lock을 우선 고려하되, 충돌률이 높으면 Pessimistic Lock이나 분산 락을 적용합니다. Optimistic Lock은 @Version을 사용해 UPDATE 시점에 버전 체크로 충돌을 감지하며, 충돌 시 ObjectOptimisticLockingFailureException을 발생시켜 재시도 로직이 필요하지만 락 대기가 없어 처리량이 높습니다. Pessimistic Lock은 SELECT FOR UPDATE로 명시적 락을 획득하여 충돌을 원천 차단하지만, 락 대기 시간이 길어지고 데드락 가능성이 있으며, 락 타임아웃 설정이 필수입니다. 분산 락은 Redis의 Redisson이나 Zookeeper를 활용해 멀티 DB 환경에서도 일관성을 보장하지만, 외부 시스템 의존성과 네트워크 레이턴시가 추가됩니다. Serializable 격리 수준은 가장 강력한 일관성을 제공하지만 성능 저하가 크므로 일반적으로 권장하지 않습니다. 실무에서는 충돌률이 낮으면 Optimistic Lock + 재고 부족 시 즉시 실패 처리, 충돌률이 높으면 Redis 분산 락 + 재고 캐싱 전략을 조합합니다.
- • Optimistic Lock은 충돌률이 낮을 때 높은 처리량 제공
- • Pessimistic Lock은 충돌을 원천 차단하지만 데드락 주의
- • 분산 락은 멀티 DB 환경에서 일관성 보장
- • Serializable 격리 수준은 성능 저하로 비권장
- • 충돌률에 따라 락 전략 선택 및 재시도 로직 구현
이커머스의 한정 수량 상품 판매나 티켓 예매 시스템에서 재고 동시성 제어는 비즈니스 정합성의 핵심입니다.
Pessimistic Lock 사용 시 데드락을 방지하기 위한 락 순서 제어 전략과 데드락 발생 시 자동 복구 방법을 설명해주세요.
Q. Spring Data JPA를 사용하는 대규모 시스템에서 N+1 문제가 발생하여 단일 API 호출에 수백 개의 쿼리가 실행되고 있습니다. Fetch Join, Entity Graph, Batch Size, 그리고 DTO Projection의 동작 원리와 각 방식의 적용 시나리오를 설명하고, 특히 페이징 처리 시 Fetch Join의 제약사항(MultipleBagFetchException, 메모리 페이징)과 해결 방법을 제시해주세요. 또한 읽기 전용 쿼리의 성능 최적화를 위한 @Transactional(readOnly=true), OSIV(Open Session In View) 패턴의 장단점, 그리고 QueryDSL과 Native Query 사용 시점을 설명해주세요.
연관 관계 로딩 전략과 쿼리 최적화 방법을 상황에 맞게 조합해야 합니다.
N+1 문제 해결의 기본은 Fetch Join으로, JPQL에서 'JOIN FETCH'를 사용해 한 번의 쿼리로 연관 엔티티를 로딩합니다. 하지만 페이징과 함께 사용 시 메모리 페이징이 발생하므로, 이 경우 @BatchSize나 hibernate.default_batch_fetch_size로 IN 절 배치 로딩을 적용합니다. ToOne 관계는 Fetch Join, ToMany 관계는 Batch Size 조합이 효과적이며, 복수의 컬렉션 Fetch Join 시 MultipleBagFetchException 발생을 주의해야 합니다. 읽기 전용 화면은 DTO Projection으로 필요한 컬럼만 조회하여 메모리 사용량을 줄이고, @Transactional(readOnly=true)로 flush 모드를 MANUAL로 설정해 변경 감지 오버헤드를 제거합니다. OSIV는 뷰 렌더링까지 영속성 컨텍스트를 유지해 Lazy Loading이 가능하지만, 커넥션 점유 시간이 길어지므로 트래픽이 높은 시스템에서는 비활성화하고 명시적 Fetch 전략을 사용합니다. 복잡한 집계나 동적 쿼리는 QueryDSL로, DB 특화 기능이 필요하면 Native Query를 사용하되 결과를 DTO로 매핑합니다.
- • Fetch Join으로 N+1 해결하되 페이징 시 Batch Size 활용
- • ToOne은 Fetch Join, ToMany는 Batch Size 조합
- • DTO Projection으로 읽기 전용 쿼리 최적화
- • @Transactional(readOnly=true)로 변경 감지 오버헤드 제거
- • OSIV는 트래픽 높은 시스템에서 비활성화 권장
복잡한 도메인 모델을 가진 시스템에서 JPA N+1 문제는 성능 저하의 가장 흔한 원인이며, 적절한 로딩 전략 선택이 필수입니다.
JPA의 1차 캐시와 2차 캐시의 동작 원리와 차이점, 그리고 2차 캐시 사용 시 주의사항을 설명해주세요.
Q. 10년 이상 운영된 Spring 레거시 시스템의 데이터베이스 스키마를 무중단으로 변경해야 하는 상황입니다. 특히 대용량 테이블에 NOT NULL 컬럼 추가, 인덱스 생성, 컬럼 타입 변경, 테이블 분리(정규화) 작업을 수행할 때, 각 작업의 락 발생 범위, 롤백 전략, 배포 순서(DB 스키마 먼저 vs 애플리케이션 먼저), 그리고 Flyway나 Liquibase 같은 마이그레이션 도구를 활용한 버전 관리 전략을 제시해주세요. 또한 마이그레이션 실패 시 데이터 정합성을 보장하는 방법과 점진적 마이그레이션(Expand-Contract 패턴) 접근법을 설명해주세요.
무중단 배포를 위해서는 스키마 변경을 여러 단계로 나누고 하위 호환성을 유지해야 합니다.
대용량 테이블의 무중단 스키마 변경은 Expand-Contract 패턴을 따릅니다. NOT NULL 컬럼 추가는 1단계로 NULL 허용 컬럼 추가 및 기본값 설정, 2단계로 애플리케이션에서 해당 컬럼 사용, 3단계로 기존 데이터 마이그레이션, 4단계로 NOT NULL 제약 추가 순으로 진행합니다. 인덱스 생성은 CONCURRENTLY 옵션(PostgreSQL) 또는 ALGORITHM=INPLACE(MySQL)를 사용해 락을 최소화하며, 대용량 테이블은 피크 시간을 피해 야간에 실행합니다. 컬럼 타입 변경은 새 컬럼 추가 후 데이터 복사, 애플리케이션 전환, 구 컬럼 삭제 순서로 점진적 전환하며, 테이블 분리는 듀얼 라이트 패턴으로 양쪽 테이블에 동시 쓰기 후 점진적으로 읽기를 전환합니다. Flyway는 버전 기반 마이그레이션 스크립트를 관리하며, 각 스크립트는 멱등성을 보장하고 롤백 스크립트를 함께 작성합니다. 배포 순서는 하위 호환 스키마 먼저 배포 후 애플리케이션 배포, 마지막으로 구 스키마 제거 순으로 진행하며, 마이그레이션 실패 시 트랜잭션 롤백과 버전 테이블 상태 확인으로 정합성을 보장합니다.
- • Expand-Contract 패턴으로 점진적 스키마 변경
- • NOT NULL 컬럼은 4단계로 나눠 추가
- • 인덱스는 CONCURRENTLY 옵션으로 락 최소화
- • 듀얼 라이트 패턴으로 테이블 분리 안전하게 수행
- • Flyway로 버전 관리 및 멱등성 보장
운영 중인 서비스의 DB 스키마 변경은 장애 리스크가 높아, 단계적 마이그레이션과 롤백 계획이 필수입니다.
데이터베이스 마이그레이션 중 롤백이 불가능한 상황(예: 데이터 손실을 동반한 변경)에서는 어떻게 안전성을 확보하시겠습니까?
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!