Node.js 미드레벨 데이터베이스 기술면접
새 면접Q. Node.js 전자상거래 애플리케이션에서 상품(Product)과 카테고리(Category) 간의 다대다(Many-to-Many) 관계를 데이터베이스에 설계해야 합니다. 중간 테이블(Junction Table)을 사용하는 방법과 JSON 컬럼을 사용하는 방법을 비교 설명하고, 각각의 장단점과 쿼리 성능에 미치는 영향, 그리고 Sequelize나 TypeORM에서 다대다 관계를 구현하는 방법을 설명해주세요.
중간 테이블은 정규화된 접근이고, JSON 컬럼은 역정규화된 접근입니다. 각각의 조인 성능과 확장성을 고려해보세요.
중간 테이블 방식은 ProductCategory 테이블을 생성하여 product_id와 category_id를 외래키로 관리하며, 정규화된 구조로 데이터 무결성이 보장되고 복잡한 필터링이 용이합니다. JSON 컬럼 방식은 Product 테이블에 category_ids JSON 배열을 저장하며, 조인 없이 한 번의 쿼리로 데이터를 가져올 수 있어 읽기 성능이 우수하지만, JSON 내부 값으로 검색 시 인덱스 활용이 제한적입니다. 중간 테이블은 카테고리별 상품 수 집계나 복잡한 필터링이 필요한 경우 적합하며, JSON 컬럼은 읽기 중심이고 카테고리 변경이 적은 경우 적합합니다. Sequelize에서는 belongsToMany 관계로 중간 테이블을 자동 생성하고, include 옵션으로 eager loading을 구현할 수 있습니다. 실무에서는 대부분 중간 테이블 방식을 선택하되, 카테고리 정보가 단순 태그 수준이고 검색 요구사항이 제한적이면 JSON 컬럼도 고려할 수 있습니다.
- • 중간 테이블은 정규화로 데이터 무결성 보장, JSON 컬럼은 조인 없는 빠른 읽기
- • 중간 테이블은 복잡한 필터링과 집계에 유리, JSON은 인덱스 활용 제한적
- • Sequelize belongsToMany로 다대다 관계 구현
- • 읽기/쓰기 패턴과 검색 요구사항에 따라 선택
상품-카테고리, 학생-수업, 사용자-권한 등 다대다 관계 모델링에서 필수적으로 사용됩니다.
중간 테이블에 추가 속성(예: 정렬 순서, 등록 일시)을 저장해야 하는 경우 Sequelize에서 어떻게 구현하나요?
Q. Node.js 애플리케이션에서 사용자 활동 로그 테이블(user_id, action_type, created_at, metadata)이 수백만 건 이상 쌓이고 있습니다. WHERE user_id = ? AND created_at BETWEEN ? AND ? 형태의 조회가 빈번한데, 단일 컬럼 인덱스 두 개를 생성하는 것과 (user_id, created_at) 복합 인덱스를 생성하는 것의 성능 차이를 설명하고, 파티셔닝(Partitioning)을 함께 고려해야 하는 이유와 구현 전략을 설명해주세요.
인덱스 머지와 범위 스캔의 효율성을 비교하고, 시계열 데이터 특성상 파티셔닝이 필요한 이유를 생각해보세요.
단일 컬럼 인덱스 두 개(user_id, created_at)를 생성하면 데이터베이스가 인덱스 머지를 시도하지만, 두 인덱스 결과의 교집합을 구하는 과정에서 오버헤드가 발생합니다. (user_id, created_at) 복합 인덱스는 user_id로 먼저 필터링한 후 정렬된 created_at 범위를 스캔하므로, 한 번의 인덱스 탐색으로 효율적으로 데이터를 찾을 수 있어 성능이 훨씬 우수합니다. 하지만 로그 테이블은 시간이 지날수록 계속 증가하므로, 인덱스 크기도 비례해서 커져 유지보수 비용이 증가합니다. 이를 해결하기 위해 created_at 기준으로 월별 또는 분기별 파티셔닝을 적용하면, 오래된 파티션은 별도 관리하거나 아카이빙할 수 있고, 쿼리 시 해당 기간 파티션만 스캔하여 성능이 개선됩니다. Node.js에서는 Sequelize나 Knex로 직접 파티션 테이블을 생성하고, 애플리케이션 레벨에서 날짜 기반 라우팅 로직을 구현하거나, PostgreSQL의 declarative partitioning 기능을 활용할 수 있습니다.
- • 복합 인덱스가 인덱스 머지보다 효율적
- • 시계열 데이터는 지속적으로 증가하여 인덱스 크기 문제 발생
- • 파티셔닝으로 오래된 데이터 분리 및 쿼리 성능 개선
- • 날짜 기준 파티셔닝과 복합 인덱스 병행 사용
대용량 로그, 이벤트 추적, 주문 이력 등 시계열 데이터를 다루는 시스템에서 필수적입니다.
파티션된 테이블에서 여러 파티션에 걸친 범위 조회가 발생할 때 성능을 최적화하는 방법은 무엇인가요?
Q. Node.js 예약 시스템에서 동일한 좌석에 대해 여러 사용자가 동시에 예약을 시도할 때, 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock)의 차이점을 설명하고, 각각의 구현 방법과 성능 특성, 그리고 Sequelize나 TypeORM에서 어떻게 구현하는지 설명해주세요. 또한 각 방식이 적합한 시나리오를 비교해주세요.
낙관적 락은 충돌이 적을 때, 비관적 락은 충돌이 빈번할 때 유리합니다. 버전 컬럼과 SELECT FOR UPDATE의 차이를 생각해보세요.
낙관적 락은 version 또는 updated_at 컬럼을 추가하여, 데이터 조회 시 버전을 읽고 업데이트 시 WHERE 조건에 버전을 포함시켜 다른 트랜잭션이 수정했는지 확인하는 방식입니다. 충돌 시 업데이트가 실패하며 애플리케이션에서 재시도 로직을 구현해야 하고, 락을 획득하지 않으므로 동시성이 높지만 충돌이 빈번하면 재시도 오버헤드가 큽니다. 비관적 락은 SELECT FOR UPDATE로 행 레벨 락을 획득하여 다른 트랜잭션이 해당 행을 수정하지 못하게 막으며, 충돌이 원천적으로 방지되지만 락 대기로 인한 성능 저하가 발생할 수 있습니다. Sequelize에서는 낙관적 락을 위해 version: true 옵션을 사용하고, 비관적 락은 transaction과 lock: true 옵션으로 구현합니다. 예약 시스템처럼 충돌 가능성이 높고 재고가 제한적인 경우 비관적 락이 적합하며, 게시글 수정처럼 충돌이 드문 경우 낙관적 락이 효율적입니다.
- • 낙관적 락은 버전 컬럼으로 충돌 감지 후 재시도
- • 비관적 락은 SELECT FOR UPDATE로 행 레벨 락 획득
- • 낙관적 락은 동시성 높지만 충돌 시 재시도 필요
- • 비관적 락은 충돌 방지하지만 대기 시간 발생
- • 시나리오별 적합한 락 전략 선택 중요
좌석 예약, 재고 차감, 포인트 차감 등 동시성 제어가 중요한 모든 시스템에서 사용됩니다.
비관적 락을 사용할 때 데드락이 발생할 수 있는 상황과 이를 방지하는 방법은 무엇인가요?
Q. Node.js 애플리케이션에서 EXPLAIN 또는 EXPLAIN ANALYZE를 사용하여 느린 쿼리를 분석할 때, 실행 계획(Execution Plan)에서 어떤 지표들을 주의 깊게 봐야 하는지 설명하고, Table Scan, Index Scan, Index Seek의 차이점과 각각이 성능에 미치는 영향을 설명해주세요. 또한 실제로 쿼리를 최적화했던 경험이나 접근 방법을 설명해주세요.
rows, cost, type 등의 지표와 Full Table Scan을 피해야 하는 이유를 중심으로 생각해보세요.
EXPLAIN 결과에서 가장 중요한 지표는 type(접근 방식), rows(예상 스캔 행 수), Extra(추가 정보)입니다. Table Scan(type: ALL)은 테이블 전체를 순차적으로 읽는 방식으로, 데이터가 많을수록 성능이 급격히 저하되므로 가능한 피해야 합니다. Index Scan은 인덱스 전체를 순차적으로 읽는 방식으로, 테이블보다는 빠르지만 여전히 모든 인덱스 항목을 읽어야 합니다. Index Seek(type: ref, range)는 인덱스의 특정 범위만 탐색하는 방식으로 가장 효율적이며, WHERE 조건에 맞는 인덱스가 있을 때 사용됩니다. Extra 필드에서 Using filesort나 Using temporary가 나타나면 정렬이나 임시 테이블 생성으로 인한 추가 비용이 발생하므로, 인덱스를 활용한 정렬로 개선할 수 있습니다. 실무에서는 먼저 EXPLAIN으로 Full Table Scan을 찾아 적절한 인덱스를 추가하고, 복합 인덱스의 컬럼 순서를 조정하거나, 불필요한 SELECT 컬럼을 제거하여 Covering Index를 활용하는 방식으로 최적화합니다.
- • type, rows, Extra 지표가 성능 분석의 핵심
- • Table Scan은 성능 저하의 주범, Index Seek가 가장 효율적
- • Using filesort, Using temporary는 추가 비용 발생 신호
- • 인덱스 추가 및 쿼리 구조 변경으로 최적화
프로덕션 환경에서 느린 쿼리를 발견했을 때 원인 분석과 최적화의 첫 단계입니다.
Covering Index가 무엇이고, 어떻게 활용하면 쿼리 성능을 개선할 수 있나요?
Q. Node.js에서 Sequelize나 TypeORM을 사용하여 분산 트랜잭션이 필요한 상황(예: 주문 생성 + 재고 차감 + 포인트 차감)을 처리할 때, 단일 데이터베이스 내 트랜잭션과 마이크로서비스 환경에서의 분산 트랜잭션의 차이를 설명하고, Saga 패턴의 개념과 보상 트랜잭션(Compensating Transaction)을 구현하는 방법을 설명해주세요.
단일 DB는 ACID 보장이 쉽지만, 분산 환경에서는 2PC나 Saga 패턴이 필요합니다.
단일 데이터베이스 내 트랜잭션은 BEGIN, COMMIT, ROLLBACK으로 ACID 속성이 자동으로 보장되며, Sequelize의 sequelize.transaction()으로 간단히 구현할 수 있습니다. 하지만 마이크로서비스 환경에서 주문 서비스, 재고 서비스, 포인트 서비스가 각각 독립된 데이터베이스를 사용하면, 하나의 트랜잭션으로 묶을 수 없어 분산 트랜잭션 문제가 발생합니다. 2PC(Two-Phase Commit)는 분산 환경에서 ACID를 보장하지만 성능 저하와 복잡도가 높아 실무에서는 거의 사용하지 않습니다. Saga 패턴은 각 서비스의 로컬 트랜잭션을 순차적으로 실행하고, 중간에 실패하면 이미 완료된 트랜잭션을 보상 트랜잭션으로 되돌리는 방식입니다. 예를 들어 주문 생성 성공 후 재고 차감 실패 시, 주문 취소 API를 호출하여 보상합니다. 구현은 Orchestration(중앙 조정자) 또는 Choreography(이벤트 기반) 방식으로 하며, 메시지 큐(RabbitMQ, Kafka)를 활용하여 이벤트를 전파하고 실패 시 보상 로직을 실행합니다.
- • 단일 DB는 트랜잭션으로 ACID 보장 용이
- • 분산 환경은 Saga 패턴으로 보상 트랜잭션 구현
- • 2PC는 복잡하고 성능 저하, 실무에서 비선호
- • 메시지 큐로 이벤트 전파 및 보상 로직 실행
마이크로서비스 아키텍처에서 여러 서비스에 걸친 비즈니스 로직을 처리할 때 필수적입니다.
Saga 패턴에서 보상 트랜잭션 자체가 실패하면 어떻게 처리해야 하나요?
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!