들어가며: 데이터베이스 면접의 중요성
백엔드 개발자, 데이터 엔지니어, 풀스택 개발자 등 다양한 IT 직군의 면접에서 데이터베이스 관련 질문은 빠지지 않고 등장합니다.
이는 현대 애플리케이션에서 데이터베이스가 차지하는 비중이 매우 크기 때문입니다.
데이터베이스에 대한 깊은 이해는 효율적인 시스템 설계와 성능 최적화에 직결되므로, 면접관들은 이 부분을 중요하게 평가합니다.
본 가이드에서는 개발자 면접에서 자주 등장하는 데이터베이스 관련 질문 10가지와 그에 대한 모범 답변을 정리했습니다.
면접 준비에 활용하시고, 실제 업무에서도 이러한 개념들을 적용하여 더 나은 개발자로 성장하시길 바랍니다.
💡 영상으로 함께 학습하기
더 자세한 설명과 암기 노트가 필요하다면? 아래 영상도 함께 확인해보세요!
[DB면접] 데이터베이스 면접 핵심질문 1편|모범답안 & 암기노트 정리 (개발자 면접 필수 CS 기본기)
질문 1: 데이터베이스의 정규화(Normalization)란 무엇이며, 왜 중요한가요?
모범 답변
정규화는 데이터베이스 설계 과정에서 중복을 최소화하고 데이터 일관성을 유지하기 위해 데이터를 구조화하는 과정입니다.
주로 관계형 데이터베이스에서 사용되며, 데이터의 중복을 제거하고 데이터 의존성을 합리적으로 관리합니다.
정규화는 1NF(제1정규형), 2NF(제2정규형), 3NF(제3정규형), BCNF(Boyce-Codd 정규형), 4NF(제4정규형), 5NF(제5정규형)으로 단계적으로 진행됩니다. 일반적으로 3NF나 BCNF까지 정규화하는 경우가 많습니다.
정규화가 중요한 이유
- 데이터 중복 감소: 같은 데이터가 여러 테이블에 중복 저장되는 것을 방지합니다.
- 데이터 무결성 향상: 데이터 변경 시 한 곳만 수정하면 되므로 일관성이 유지됩니다.
- 데이터베이스 구조 명확화: 각 테이블의 목적이 명확해져 유지보수가 용이합니다.
- 갱신 이상(Anomaly) 방지: 삽입, 삭제, 갱신 이상 현상을 예방합니다.
실제 예시
-- 정규화 전 (학생 정보와 수강 과목이 한 테이블에 있는 경우)
CREATE TABLE StudentCourses (
student_id INT,
student_name VARCHAR(100),
student_address VARCHAR(200),
course_id INT,
course_name VARCHAR(100),
instructor_name VARCHAR(100)
);
-- 정규화 후 (3NF)
CREATE TABLE Students (
student_id INT PRIMARY KEY,
student_name VARCHAR(100),
student_address VARCHAR(200)
);
CREATE TABLE Courses (
course_id INT PRIMARY KEY,
course_name VARCHAR(100),
instructor_name VARCHAR(100)
);
CREATE TABLE Enrollments (
student_id INT,
course_id INT,
PRIMARY KEY (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES Students(student_id),
FOREIGN KEY (course_id) REFERENCES Courses(course_id)
);
다만, 과도한 정규화는 조인 연산이 많아져 성능 저하를 일으킬 수 있으므로,
실무에서는 상황에 맞게 적절한 수준의 정규화와 역정규화(Denormalization)를 함께 고려합니다.
질문 2: ACID 속성에 대해 설명해주세요
모범 답변
ACID는 데이터베이스 트랜잭션이 안전하게 수행되는 것을 보장하기 위한 4가지 속성의 두문자어입니다:
1. 원자성(Atomicity)
트랜잭션의 모든 연산은 전부 성공하거나 전부 실패해야 합니다. 중간 상태는 존재하지 않습니다.
- 예: 계좌 이체 시 출금과 입금이 모두 성공하거나, 문제 발생 시 모두 롤백되어야 합니다.
2. 일관성(Consistency)
트랜잭션 전후로 데이터베이스는 일관된 상태를 유지해야 합니다. 모든 무결성 제약조건을 만족해야 합니다.
- 예: 계좌 잔액은 항상 0 이상이어야 한다는 규칙이 있다면, 트랜잭션 후에도 이 규칙이 지켜져야 합니다.
3. 격리성(Isolation)
동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않아야 합니다. 마치 각 트랜잭션이 순차적으로 실행되는 것처럼 동작해야 합니다.
- 예: 사용자 A와 B가 동시에 같은 데이터를 수정할 때 서로의 작업이 충돌하지 않도록 보장합니다.
4. 지속성(Durability)
성공적으로 완료된 트랜잭션의 결과는 시스템 장애가 발생하더라도 영구적으로 반영되어야 합니다.
- 예: 은행 거래가 완료된 후 시스템이 다운되더라도, 재시작 후에도 해당 거래 내역이 보존되어야 합니다.
실제 구현 예시
// Java에서 트랜잭션 처리 예시 (Spring Framework)
@Transactional
public void transferMoney(long fromId, long toId, BigDecimal amount) {
Account fromAccount = accountRepository.findById(fromId)
.orElseThrow(() -> new AccountNotFoundException(fromId));
Account toAccount = accountRepository.findById(toId)
.orElseThrow(() -> new AccountNotFoundException(toId));
// 원자성: 두 작업 모두 성공하거나 모두 실패
fromAccount.withdraw(amount); // 출금 실패 시 예외 발생
toAccount.deposit(amount);
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
// 메서드가 정상 종료되면 트랜잭션 커밋, 예외 발생 시 롤백
}
ACID 속성은 주로 관계형 데이터베이스(RDBMS)에서 강조되며, 데이터 무결성과 신뢰성이 중요한 금융, 의료 등의 분야에서 특히 중요합니다. 반면, NoSQL 데이터베이스는 확장성과 성능을 위해 이러한 속성의 일부를 완화하는 경향이 있습니다.
질문 3: RDBMS와 NoSQL의 차이점은 무엇인가요?
모범 답변
RDBMS(관계형 데이터베이스 관리 시스템)와 NoSQL(Not Only SQL) 데이터베이스는 데이터 저장 및 처리 방식에 근본적인 차이가 있습니다:
RDBMS 특징
- 구조화된 스키마: 미리 정의된 스키마에 따라 데이터가 테이블, 행, 열의 형태로 저장됩니다.
- 관계형 모델: 테이블 간 관계를 외래 키로 표현하고, JOIN 연산으로 데이터를 조합합니다.
- ACID 트랜잭션: 데이터 무결성과 안정성을 보장합니다.
- SQL 쿼리 언어: 표준화된 SQL을 사용해 데이터를 조작합니다.
- 수직적 확장(Scale-up): 주로 하드웨어 성능 향상으로 확장합니다.
- 예시: MySQL, PostgreSQL, Oracle, SQL Server
NoSQL 특징
- 유연한 스키마: 스키마 없이 또는 동적 스키마로 데이터를 저장할 수 있습니다.
- 다양한 데이터 모델: 문서형, 키-값, 컬럼형, 그래프형 등 다양한 모델이 있습니다.
- BASE 원칙: 가용성과 확장성을 위해 일관성을 일부 타협합니다(Basically Available, Soft state, Eventually consistent).
- 수평적 확장(Scale-out): 서버를 추가하는 방식으로 쉽게 확장 가능합니다.
- 대용량 데이터 처리: 빅데이터와 실시간 웹 애플리케이션에 적합합니다.
- 예시: MongoDB(문서형), Redis(키-값), Cassandra(컬럼형), Neo4j(그래프형)
선택 기준
고려 사항 | RDBMS 선택 | NoSQL 선택 |
---|---|---|
데이터 구조 | 구조화된, 변경이 적은 데이터 | 비구조화/반구조화 데이터, 가변적 구조 |
확장성 | 중소 규모, 수직 확장 필요 | 대규모, 수평 확장 필요 |
트랜잭션 | 복잡한 트랜잭션, 데이터 무결성 중요 | 단순한 트랜잭션, 고가용성 중요 |
쿼리 | 복잡한 조인과 집계 필요 | 단순하고 빠른 키 기반 접근 필요 |
일관성 | 강한 일관성 필요 | 최종 일관성으로 충분 |
실제 사용 사례
-- RDBMS 적합 사례: 은행 거래 시스템
-- 계좌 간 이체 시 ACID 트랜잭션으로 데이터 무결성 보장
-- 복잡한 관계와 제약조건(외래 키, 체크 제약 등) 필요
-- SQL로 다양한 보고서 및 분석 쿼리 실행
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
COMMIT;
// NoSQL 적합 사례: 소셜 미디어 피드
// 사용자마다 다른 구조의 콘텐츠 저장 필요
// 수평적 확장으로 수백만 사용자 동시 처리
// 읽기 작업이 많고 일관성보다 가용성이 중요
// MongoDB 문서 저장 예시
{
"_id": "post123",
"userId": "user456",
"content": "오늘 날씨가 좋네요!",
"media": [
{"type": "image", "url": "photo.jpg"},
{"type": "video", "url": "video.mp4"}
],
"likes": 127,
"comments": [...],
"timestamp": "2024-01-15T10:30:00Z"
}
현대 시스템에서는 폴리글랏 퍼시스턴스(Polyglot Persistence) 접근 방식을 채택해,
한 애플리케이션 내에서 데이터 특성에 따라 RDBMS와 NoSQL을 함께 사용하는 경우가 증가하고 있습니다.
질문 4: 인덱스(Index)란 무엇이며, 어떻게 작동하나요?
모범 답변
인덱스(Index)는 데이터베이스 테이블의 검색 속도를 향상시키기 위한 자료구조입니다.
책의 색인과 유사하게, 데이터의 위치를 빠르게 찾을 수 있도록 도와줍니다.
인덱스의 작동 원리
- 인덱스는 테이블의 하나 이상의 열에 대한 정렬된 사본을 별도의 구조로 유지합니다.
- 가장 일반적인 인덱스 구조는 B-Tree(Balanced Tree)와 B+Tree입니다.
- 인덱스에는 키(인덱싱된 컬럼 값)와 해당 레코드의 포인터(ROWID)가 저장됩니다.
- 쿼리 실행 시 DBMS는 테이블 전체를 스캔하는 대신 인덱스를 검색하여 필요한 데이터의 위치를 빠르게 찾습니다.
인덱스 종류
- 단일 컬럼 인덱스: 하나의 열에 대해 생성됩니다.
- 복합 인덱스: 두 개 이상의 열에 대해 생성됩니다.
- 고유 인덱스: 중복 값을 허용하지 않습니다(UNIQUE INDEX).
- 클러스터드 인덱스: 실제 데이터가 인덱스 키 순서대로 물리적으로 정렬됩니다(테이블당 하나만 가능).
- 비클러스터드 인덱스: 데이터와 별도로 인덱스가 유지됩니다(여러 개 가능).
- 함수 기반 인덱스: 열의 값이 아닌 함수의 결과에 대한 인덱스입니다.
- 부분 인덱스: 조건을 만족하는 행에만 인덱스를 생성합니다.
인덱스 생성 및 사용 예시
-- 단일 컬럼 인덱스 생성
CREATE INDEX idx_last_name ON employees(last_name);
-- 복합 인덱스 생성
CREATE INDEX idx_name_email ON users(last_name, first_name, email);
-- 고유 인덱스 생성
CREATE UNIQUE INDEX idx_email ON users(email);
-- 함수 기반 인덱스 생성
CREATE INDEX idx_upper_name ON products(UPPER(product_name));
-- 부분 인덱스 생성 (PostgreSQL)
CREATE INDEX idx_active_users ON users(email) WHERE status = 'active';
-- 인덱스를 활용하는 쿼리
SELECT * FROM employees WHERE last_name = 'Kim';
-- 인덱스를 활용하는 복합 조건 쿼리
SELECT * FROM users
WHERE last_name = 'Lee' AND first_name = 'Minho';
-- 쿼리 실행 계획 확인
EXPLAIN SELECT * FROM employees WHERE last_name = 'Kim';
인덱스의 장단점
장점
- 쿼리 검색 속도 향상(특히 WHERE, JOIN, ORDER BY, GROUP BY 절)
- 유니크 제약 조건 적용에 도움
- 정렬 및 그룹화 연산 가속화
단점
- 추가 저장 공간 필요
- INSERT, UPDATE, DELETE 작업 시 인덱스 갱신 오버헤드
- 불필요하게 많은 인덱스는 성능 저하 초래
인덱스 생성 고려 사항
- 조회가 빈번한 열에 인덱스 생성
- 카디널리티(고유한 값의 수)가 높은 열에 효과적
- 외래 키 열에 인덱스 생성 고려
- 인덱스 사용 여부 확인을 위해 쿼리 실행 계획(EXPLAIN) 활용
- WHERE 절에서 자주 사용되는 열 조합으로 복합 인덱스 생성
인덱스는 성능 튜닝의 가장 중요한 도구 중 하나이지만, 상황에 맞게 적절히 사용해야 합니다.
과도한 인덱싱은 오히려 성능을 저하시킬 수 있습니다.
질문 5: 트랜잭션 격리 수준(Transaction Isolation Level)에 대해 설명해주세요
모범 답변
트랜잭션 격리 수준은 동시에 실행되는 여러 트랜잭션이 서로 영향을 미치는 정도를 제어하는 설정입니다.
SQL 표준(ANSI/ISO SQL)은 네 가지 격리 수준을 정의하며, 각 수준은 성능과 데이터 일관성 사이의 균형을 다르게 제공합니다.
격리 수준과 발생 가능한 현상
격리 수준 | Dirty Read | Non-repeatable Read | Phantom Read |
---|---|---|---|
READ UNCOMMITTED | 가능 | 가능 | 가능 |
READ COMMITTED | 방지 | 가능 | 가능 |
REPEATABLE READ | 방지 | 방지 | 가능 |
SERIALIZABLE | 방지 | 방지 | 방지 |
1. READ UNCOMMITTED (가장 낮은 수준)
- 다른 트랜잭션이 커밋하지 않은 데이터를 읽을 수 있습니다(Dirty Read).
- 성능은 가장 좋지만 데이터 일관성이 보장되지 않습니다.
- 예시 상황: 사용자 A가 데이터를 수정 중이고 아직 커밋하지 않았는데, 사용자 B가 수정 중인 데이터를 읽을 수 있습니다.
2. READ COMMITTED
- 커밋된 데이터만 읽을 수 있어 Dirty Read는 방지됩니다.
- 하지만 한 트랜잭션 내에서 같은 쿼리를 두 번 실행할 때 결과가 달라질 수 있습니다(Non-repeatable Read).
- 많은 DBMS의 기본 격리 수준(Oracle, SQL Server, PostgreSQL 등)입니다.
-- 트랜잭션 1
BEGIN;
SELECT price FROM products WHERE id = 1; -- 가격: 1000원
-- 이 시점에 트랜잭션 2가 가격을 2000원으로 변경하고 커밋
SELECT price FROM products WHERE id = 1; -- 가격: 2000원 (변경됨)
COMMIT;
3. REPEATABLE READ
- 트랜잭션 내에서 같은 행을 여러 번 읽어도 동일한 결과를 보장합니다(Non-repeatable Read 방지).
- 하지만 다른 트랜잭션이 새로운 행을 삽입하면 이를 감지할 수 있습니다(Phantom Read).
- MySQL InnoDB의 기본 격리 수준입니다.
-- 트랜잭션 1
BEGIN;
SELECT COUNT(*) FROM products WHERE price > 1000; -- 결과: 5개
-- 이 시점에 트랜잭션 2가 조건에 맞는 새 제품을 추가하고 커밋
SELECT COUNT(*) FROM products WHERE price > 1000; -- 결과: 6개 (팬텀 읽기)
COMMIT;
4. SERIALIZABLE (가장 높은 수준)
- 모든 트랜잭션이 순차적으로 실행되는 것처럼 동작하여 가장 강력한 일관성을 제공합니다.
- 모든 동시성 문제(Dirty Read, Non-repeatable Read, Phantom Read)를 방지합니다.
- 그러나 성능 저하가 크고 교착 상태(Deadlock)의 가능성이 높아집니다.
- 금융 거래 등 데이터 정확성이 절대적으로 중요한 경우에 사용합니다.
실제 설정 방법
-- MySQL에서 트랜잭션 격리 수준 설정
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- PostgreSQL에서 특정 트랜잭션의 격리 수준 설정
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- SQL 문 실행
COMMIT;
-- 현재 격리 수준 확인
SELECT @@transaction_isolation; -- MySQL
SHOW transaction_isolation; -- PostgreSQL
격리 수준 선택 기준
- 애플리케이션의 일관성 요구 사항
- 동시 사용자 수와 동시성 부하
- 읽기/쓰기 작업의 비율
- 비즈니스 로직의 중요도
실무에서는 READ COMMITTED와 REPEATABLE READ가 가장 많이 사용되며, 상황에 따라 적절한 격리 수준을 선택하는 것이 중요합니다. 또한 DBMS별로 구현 방식이 다를 수 있으므로 사용 중인 데이터베이스의 공식 문서를 참고하는 것이 좋습니다.
질문 6: JOIN의 종류와 각각의 용도를 설명해주세요
모범 답변
JOIN은 관계형 데이터베이스에서 두 개 이상의 테이블을 연결하여 데이터를 조회하는 연산입니다.
테이블 간의 관계를 기반으로 다양한 JOIN 유형이 존재합니다.
주요 JOIN 유형
1. INNER JOIN (내부 조인)
- 두 테이블에서 조인 조건을 만족하는 행만 반환합니다.
- 양쪽 테이블 모두에 일치하는 데이터가 있는 경우에만 결과에 포함됩니다.
- 가장 흔히 사용되는 조인 유형입니다.
SELECT e.employee_name, d.department_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.department_id;
2. LEFT JOIN (LEFT OUTER JOIN, 왼쪽 외부 조인)
- 왼쪽 테이블의 모든 행과 오른쪽 테이블에서 조건에 맞는 행을 반환합니다.
- 오른쪽 테이블에 일치하는 항목이 없으면 NULL로 표시됩니다.
- 참조 무결성 검사나 누락된 관계를 찾을 때 유용합니다.
SELECT c.customer_name, o.order_id, o.order_date
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id;
-- 주문이 없는 고객도 결과에 포함됩니다
3. RIGHT JOIN (RIGHT OUTER JOIN, 오른쪽 외부 조인)
- 오른쪽 테이블의 모든 행과 왼쪽 테이블에서 조건에 맞는 행을 반환합니다.
- 왼쪽 테이블에 일치하는 항목이 없으면 NULL로 표시됩니다.
- LEFT JOIN과 테이블 순서만 바뀐 형태이며, 실무에서는 일관성을 위해 LEFT JOIN을 더 자주 사용합니다.
SELECT p.product_name, o.order_id
FROM order_items o
RIGHT JOIN products p ON o.product_id = p.product_id;
-- 주문되지 않은 제품도 결과에 포함됩니다
4. FULL JOIN (FULL OUTER JOIN, 전체 외부 조인)
- 양쪽 테이블의 모든 행을 반환합니다. 일치하지 않는 행의 경우 반대쪽 테이블의 열은 NULL로 표시됩니다.
- 두 데이터 집합 간의 모든 관계를 보고 싶을 때 유용합니다.
- 일부 DBMS(MySQL 등)에서는 직접 지원하지 않을 수 있으며, LEFT JOIN과 RIGHT JOIN을 UNION으로 조합하여 구현합니다.
SELECT e.employee_id, e.employee_name, p.project_id, p.project_name
FROM employees e
FULL JOIN projects p ON e.employee_id = p.manager_id;
-- 프로젝트가 없는 직원, 매니저가 없는 프로젝트 모두 결과에 포함
-- MySQL에서 FULL JOIN 구현
SELECT e.employee_id, e.employee_name, p.project_id, p.project_name
FROM employees e
LEFT JOIN projects p ON e.employee_id = p.manager_id
UNION
SELECT e.employee_id, e.employee_name, p.project_id, p.project_name
FROM employees e
RIGHT JOIN projects p ON e.employee_id = p.manager_id;
5. CROSS JOIN (교차 조인)
- 두 테이블의 모든 가능한 조합을 반환합니다(카티션 곱).
- 조인 조건이 없으며, 첫 번째 테이블의 각 행이 두 번째 테이블의 모든 행과 결합됩니다.
- 결과 행의 수는 두 테이블 행 수의 곱과 같습니다.
- 가능한 모든 조합을 생성할 때 유용하지만, 대용량 테이블에서는 성능 문제가 발생할 수 있습니다.
SELECT p.product_name, c.color_name
FROM products p
CROSS JOIN colors c;
-- 각 제품과 각 색상의 모든 조합 생성
6. SELF JOIN (자체 조인)
- 동일한 테이블을 자기 자신과 조인합니다.
- 테이블 내에서 행 간의 관계를 찾을 때 유용합니다.
- 계층 구조(관리자-직원 관계 등)를 표현할 때 많이 사용됩니다.
SELECT e1.employee_name AS employee, e2.employee_name AS manager
FROM employees e1
JOIN employees e2 ON e1.manager_id = e2.employee_id;
-- 각 직원과 해당 관리자를 조회
7. NATURAL JOIN (자연 조인)
- 두 테이블에서 이름이 같은 모든 열을 기준으로 자동으로 조인합니다.
- 조인 조건을 명시적으로 지정하지 않아도 됩니다.
- 테이블 구조가 변경될 경우 예상치 못한 결과가 나올 수 있어 주의가 필요합니다.
SELECT employee_name, department_name
FROM employees
NATURAL JOIN departments;
-- 두 테이블에서 이름이 같은 열(예: department_id)을 기준으로 자동 조인
JOIN 선택 시 고려사항
- 데이터 관계: 어떤 데이터를 조회해야 하는지에 따라 조인 유형 결정
- 성능: 대용량 테이블의 경우 적절한 인덱스와 조인 전략 필요
- 가독성: 명시적인 조인 조건(ON 절)을 사용하여 코드 의도를 분명히 함
- 데이터 품질: 외부 조인 사용 시 NULL 값 처리 방법 고려
JOIN을 활용한 실전 쿼리 예시
-- 여러 테이블 조인 및 집계 함수 활용
SELECT c.category_name,
COUNT(p.product_id) AS product_count,
AVG(p.price) AS avg_price
FROM categories c
LEFT JOIN products p ON c.category_id = p.category_id
LEFT JOIN order_items oi ON p.product_id = oi.product_id
GROUP BY c.category_name
ORDER BY product_count DESC;
-- 서브쿼리와 조인 결합
SELECT e.employee_name, e.salary,
d.department_name,
(SELECT AVG(salary) FROM employees
WHERE department_id = e.department_id) AS dept_avg_salary
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.salary > (
SELECT AVG(salary) FROM employees
);
JOIN은 관계형 데이터베이스의 핵심 기능이며, 효율적인 JOIN 작성 능력은 데이터베이스 개발자에게 필수적인 역량입니다.
질문 7: 데이터베이스 샤딩(Sharding)이란 무엇인가요?
모범 답변
데이터베이스 샤딩(Sharding)은 대규모 데이터베이스를 여러 개의 작은 파티션(샤드)으로 분할하여 수평적으로 확장하는 기법입니다.
각 샤드는 동일한 스키마를 가지지만 서로 다른 데이터 세트를 포함합니다.
샤딩의 주요 목적
- 성능 향상: 각 샤드가 더 적은 양의 데이터를 처리하므로 쿼리 성능이 향상됩니다.
- 확장성 개선: 서버를 추가하여 시스템 용량을 쉽게 확장할 수 있습니다.
- 가용성 증가: 한 샤드에 장애가 발생해도 전체 시스템이 중단되지 않습니다.
- 지리적 분산: 사용자와 가까운 위치에 데이터를 배치할 수 있습니다.
샤딩 전략
1. 해시 기반 샤딩(Hash-Based Sharding)
- 샤딩 키(예: 사용자 ID)의 해시 값을 계산하여 데이터를 분산합니다.
- 균등한 데이터 분포를 제공하지만, 샤드 추가 시 재샤딩이 복잡할 수 있습니다.
# 해시 기반 샤딩 예시
shard_id = hash(user_id) % number_of_shards
2. 범위 기반 샤딩(Range-Based Sharding)
- 특정 범위의 값(예: 지역별, 날짜별)에 따라 데이터를 분할합니다.
- 구현이 간단하고 특정 범위 쿼리가 효율적이지만, 데이터 불균형이 발생할 수 있습니다.
-- 범위 기반 샤딩 예시
-- if user_id between 1 and 10000: shard_1
-- else if user_id between 10001 and 20000: shard_2
-- ...
3. 디렉토리 기반 샤딩(Directory-Based Sharding)
- 별도의 조회 서비스가 각 키가 어떤 샤드에 있는지 추적합니다.
- 유연한 데이터 분산이 가능하지만, 룩업 서비스가 단일 장애 지점이 될 수 있습니다.
4. 지리적 샤딩(Geo-Based Sharding)
- 사용자의 지리적 위치에 따라 데이터를 분산합니다.
- 지역별 사용자에게 낮은 지연 시간을 제공할 수 있습니다.
샤딩 구현 예시
// 간단한 해시 기반 샤딩 구현(Java)
public class ShardingRouter {
private final int SHARD_COUNT = 4;
private final DataSource[] shards = new DataSource[SHARD_COUNT];
// 샤딩 키에 따라 적절한 데이터소스 결정
public DataSource getShardByUserId(long userId) {
int shardIndex = (int) (userId % SHARD_COUNT);
return shards[shardIndex];
}
// 쿼리 실행
public User getUserById(long userId) {
DataSource shard = getShardByUserId(userId);
// 선택된 샤드에서 사용자 조회
return executeQueryOnShard(shard, "SELECT * FROM users WHERE id = ?", userId);
}
// 모든 샤드에서 데이터 조회 (주의: 성능 이슈 가능)
public List<User> getAllActiveUsers() {
List<User> allUsers = new ArrayList<>();
for (DataSource shard : shards) {
List<User> users = executeQueryOnShard(shard,
"SELECT * FROM users WHERE status = 'active'");
allUsers.addAll(users);
}
return allUsers;
}
}
샤딩의 장단점
장점:
- 시스템 처리량 및 저장 용량 증가
- 쿼리 응답 시간 감소
- 장애 격리로 인한 가용성 향상
- 지리적 분산 지원
단점:
- 조인 및 트랜잭션의 복잡성 증가(샤드 간 조인은 어려움)
- 샤드 간 데이터 균형 유지의 어려움
- 샤드 추가/제거 시 데이터 재배치 필요
- 설계 및 관리 복잡성 증가
샤딩 사용 사례
- SNS 플랫폼: 사용자 ID를 기준으로 샤딩하여 수십억 사용자 데이터 관리
- 전자상거래: 지역별 또는 제품 카테고리별 샤딩으로 대규모 제품 카탈로그 관리
- 게임 서비스: 게임 서버 또는 지역별로 샤딩하여 동시 접속자 처리
- 로그 및 모니터링: 시간별 샤딩으로 대량의 로그 데이터 효율적 저장
-- 시간 기반 샤딩 예시 (로그 데이터)
-- 2024년 1월 데이터: logs_2024_01
-- 2024년 2월 데이터: logs_2024_02
CREATE TABLE logs_2024_01 (
id BIGINT PRIMARY KEY,
timestamp DATETIME,
user_id INT,
action VARCHAR(100),
details JSON
);
샤딩은 대규모 시스템의 성능과 확장성 요구사항을 충족시키는 중요한 기술이지만, 가능하면 단일 데이터베이스 최적화부터 시도하고 정말 필요할 때 도입하는 것이 좋습니다. 또한 MongoDB, Cassandra 등 많은 NoSQL 데이터베이스는 내장된 샤딩 기능을 제공합니다.
질문 8: N+1 문제란 무엇이며, 어떻게 해결할 수 있나요?
모범 답변
N+1 문제는 ORM(Object-Relational Mapping)을 사용할 때 자주 발생하는 성능 이슈로,
부모 엔티티를 조회한 후 각 부모 엔티티의 자식 엔티티를 개별적으로 추가 쿼리하여 발생하는 문제입니다.
1개의 쿼리로 부모 엔티티 N개를 조회한 후, N개의 부모 각각에 대해 자식을 조회하는 쿼리가 추가로 실행되어 총 N+1개의 쿼리가 발생합니다.
문제 발생 예시 (Java + JPA/Hibernate)
// 엔티티 정의
@Entity
public class Post {
@Id
private Long id;
private String title;
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
private List<Comment> comments;
// ...
}
@Entity
public class Comment {
@Id
private Long id;
private String content;
@ManyToOne
private Post post;
// ...
}
// N+1 문제 발생 코드
List<Post> posts = entityManager.createQuery("SELECT p FROM Post p", Post.class).getResultList();
// 여기까지 1개의 쿼리 실행
// 각 게시물의 댓글에 접근할 때마다 추가 쿼리 실행
for (Post post : posts) {
System.out.println("Post: " + post.getTitle() + ", Comments: " + post.getComments().size());
// 각 post마다 comments를 로딩하는 쿼리 실행 -> N개의 추가 쿼리
}
위 코드는 다음과 같은 쿼리를 발생시킵니다
SELECT * FROM posts
(모든 게시물 조회)SELECT * FROM comments WHERE post_id = 1
(첫 번째 게시물의 댓글)SELECT * FROM comments WHERE post_id = 2
(두 번째 게시물의 댓글)- ... (게시물 수만큼 반복)
N+1 문제 해결 방법
1. 즉시 로딩(Eager Loading) 사용
- 자식 엔티티를 부모와 함께 한 번에 조회합니다.
- JPA의
FetchType.EAGER
를 사용할 수 있지만, 항상 자식을 함께 로딩하므로 불필요한 경우도 있어 권장되지 않습니다.
@OneToMany(mappedBy = "post", fetch = FetchType.EAGER)
private List<Comment> comments;
2. 조인 페치(Join Fetch) / 선택적 패치 조인
- JPQL이나 Criteria API에서 JOIN FETCH를 사용하여 필요할 때만 연관 엔티티를 함께 조회합니다.
// JPQL을 사용한 Join Fetch
List<Post> posts = entityManager
.createQuery("SELECT p FROM Post p JOIN FETCH p.comments", Post.class)
.getResultList();
// Spring Data JPA를 사용한 경우
@Query("SELECT p FROM Post p JOIN FETCH p.comments")
List<Post> findAllWithComments();
3. 엔티티 그래프(Entity Graph)
- JPA 2.1부터 도입된 기능으로, 엔티티와 연관 관계를 함께 조회할 수 있습니다.
@EntityGraph(attributePaths = {"comments"})
@Query("SELECT p FROM Post p")
List<Post> findAllWithCommentsGraph();
// 동적 엔티티 그래프
EntityGraph<Post> entityGraph = entityManager.createEntityGraph(Post.class);
entityGraph.addAttributeNodes("comments");
TypedQuery<Post> query = entityManager.createQuery("SELECT p FROM Post p", Post.class);
query.setHint("javax.persistence.fetchgraph", entityGraph);
List<Post> posts = query.getResultList();
4. 배치 페치 사이즈(Batch Fetch Size) 설정
- Hibernate의
@BatchSize
또는hibernate.default_batch_fetch_size
속성을 사용하여 N+1개의 쿼리를 더 적은 수의 쿼리로 통합합니다.
// 엔티티 클래스에 BatchSize 지정
@Entity
public class Post {
// ...
@OneToMany(mappedBy = "post")
@BatchSize(size = 20)
private List<Comment> comments;
}
# application.properties에 전역 설정
spring.jpa.properties.hibernate.default_batch_fetch_size=20
5. DTO 프로젝션(Projection) 사용
- 필요한 데이터만 직접 선택하여 DTO로 반환합니다.
// DTO 클래스 정의
public class PostSummaryDTO {
private Long id;
private String title;
private Long commentCount;
// 생성자, getter, setter
}
// Repository에서 DTO 프로젝션 사용
@Query("SELECT new com.example.PostSummaryDTO(p.id, p.title, COUNT(c)) " +
"FROM Post p LEFT JOIN p.comments c GROUP BY p.id, p.title")
List<PostSummaryDTO> findPostSummaries();
6. 네이티브 SQL 사용
- 복잡한 경우 네이티브 SQL로 최적화된 쿼리를 직접 작성할 수 있습니다.
@Query(value = "SELECT p.*, c.* FROM posts p LEFT JOIN comments c ON p.id = c.post_id",
nativeQuery = true)
List<Object[]> findPostsWithCommentsNative();
해결책 선택 시 고려사항
- 데이터 사용 패턴: 자식 엔티티가 항상 필요한지 또는 가끔 필요한지 고려
- 데이터 양: 자식 엔티티의 수가 많은 경우 페이징이나 필터링 고려
- 쿼리 복잡성: 너무 복잡한 조인은 성능 저하를 일으킬 수 있음
- 메모리 사용: 대량의 데이터를 한 번에 로딩하면 메모리 사용량 증가
실제 개선 사례
// Before: N+1 문제 발생
public List<PostDTO> getAllPosts() {
List<Post> posts = postRepository.findAll(); // 1개 쿼리
return posts.stream()
.map(post -> PostDTO.builder()
.id(post.getId())
.title(post.getTitle())
.commentCount(post.getComments().size()) // N개 쿼리 발생
.build())
.collect(Collectors.toList());
}
// After: JOIN FETCH로 해결
public List<PostDTO> getAllPosts() {
List<Post> posts = postRepository.findAllWithComments(); // 1개 쿼리
return posts.stream()
.map(post -> PostDTO.builder()
.id(post.getId())
.title(post.getTitle())
.commentCount(post.getComments().size()) // 추가 쿼리 없음
.build())
.collect(Collectors.toList());
}
N+1 문제는 ORM 사용 시 자주 발생하는 성능 문제이므로, 코드 리뷰와 성능 테스트 과정에서 발견하고 수정하는 것이 중요합니다.
개발 초기 단계부터 적절한 페치 전략을 설계하면 나중에 리팩토링하는 수고를 줄일 수 있습니다.
질문 9: SQL Injection은 무엇이며, 어떻게 방지할 수 있나요?
모범 답변
SQL Injection은 악의적인 사용자가 애플리케이션의 입력 필드를 통해 악성 SQL 코드를 삽입하여 데이터베이스를 비정상적으로 조작하는 보안 취약점입니다. 이를 통해 공격자는 데이터베이스에서 정보를 유출하거나, 수정하거나, 심지어 삭제할 수도 있습니다.
SQL Injection 공격 예시
1. 기본적인 SQL Injection
// 취약한 로그인 쿼리
String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
// 공격자 입력: username = admin' --
// 결과 쿼리: SELECT * FROM users WHERE username = 'admin' -- AND password = ''
// 주석 처리로 비밀번호 검증을 우회하여 관리자 계정으로 로그인
2. UNION 기반 공격
// 취약한 제품 검색 쿼리
String query = "SELECT name, price FROM products WHERE category = '" + category + "'";
// 공격자 입력: category = ' UNION SELECT username, password FROM users --
// 결과 쿼리: SELECT name, price FROM products WHERE category = '' UNION SELECT username, password FROM users --'
// 사용자 테이블의 계정 정보가 결과에 포함됨
3. Blind SQL Injection
-- 공격자 입력: id = 1 AND 1=1 -- 참이면 정상 결과 반환
-- 공격자 입력: id = 1 AND 1=0 -- 거짓이면 결과 없음
-- 공격자는 이러한 참/거짓 응답을 통해 데이터베이스 정보를 추측
SQL Injection 방지 방법
1. 준비된 문장(Prepared Statements) 사용
- 쿼리와 데이터를 분리하여 SQL 구문 분석을 미리 수행합니다.
- 사용자 입력이 SQL 코드로 해석되지 않고 순수한 데이터로만 처리됩니다.
// Java에서 PreparedStatement 사용 예
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
// Node.js에서 prepared statement
const sql = 'SELECT * FROM users WHERE username = ? AND password = ?';
connection.query(sql, [username, password], function (error, results, fields) {
// 결과 처리
});
# Python에서 parameterized query
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))
2. ORM(Object-Relational Mapping) 프레임워크 활용
- Hibernate, Django ORM, Entity Framework 등의 ORM은 일반적으로 SQL Injection 방지 기능이 내장되어 있습니다.
// Java에서 JPA/Hibernate 사용
User user = entityManager
.createQuery("SELECT u FROM User u WHERE u.username = :username", User.class)
.setParameter("username", username)
.getSingleResult();
# Python에서 Django ORM 사용
user = User.objects.filter(username=username).first()
3. 저장 프로시저(Stored Procedures) 사용
- 데이터베이스에 미리 정의된 프로시저를 호출하여 입력 값을 매개변수로 전달합니다.
-- 저장 프로시저 정의
CREATE PROCEDURE sp_GetUserByCredentials
@Username NVARCHAR(50),
@Password NVARCHAR(50)
AS
BEGIN
SELECT * FROM Users WHERE Username = @Username AND Password = @Password
END
-- 애플리케이션에서 호출
EXEC sp_GetUserByCredentials @Username = 'user_input', @Password = 'pass_input'
4. 입력 유효성 검사 및 필터링
- 사용자 입력을 서버 측에서 철저히 검증합니다.
- 특수 문자, SQL 키워드 등을 필터링하거나 이스케이프 처리합니다.
// JavaScript에서 입력 검증 예시
function isValidUsername(username) {
// 영문자와 숫자만 허용하는 정규식
return /^[a-zA-Z0-9]+$/.test(username);
}
function sanitizeInput(input) {
// 위험한 문자들을 제거 또는 이스케이프
return input.replace(/['"\\;]/g, '');
}
if (!isValidUsername(userInput)) {
return res.status(400).send('유효하지 않은 사용자명입니다.');
}
5. 최소 권한 원칙 적용
- 애플리케이션에서 사용하는 데이터베이스 계정에 필요한 최소한의 권한만 부여합니다.
- 읽기 전용 작업에는 SELECT 권한만 있는 계정을 사용합니다.
-- 제한된 권한을 가진 사용자 생성
CREATE USER 'app_read_user'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT ON app_db.* TO 'app_read_user'@'localhost';
-- 쓰기 작업용 계정 (필요한 테이블에만 권한 부여)
CREATE USER 'app_write_user'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT, INSERT, UPDATE ON app_db.orders TO 'app_write_user'@'localhost';
GRANT SELECT, INSERT, UPDATE ON app_db.order_items TO 'app_write_user'@'localhost';
6. 에러 메시지 제한
- 상세한 데이터베이스 오류 메시지를 사용자에게 노출하지 않습니다.
- 오류 로깅은 서버에만 기록하고, 사용자에게는 일반적인 메시지만 표시합니다.
// 나쁜 예: 상세한 에러 노출
try {
// 데이터베이스 작업
} catch(SQLException e) {
return "SQL Error: " + e.getMessage(); // 공격자에게 유용한 정보 제공
}
// 좋은 예: 일반적인 에러 메시지
try {
// 데이터베이스 작업
} catch(SQLException e) {
logger.error("Database error: ", e); // 서버 로그에만 기록
return "요청을 처리하는 중 오류가 발생했습니다."; // 일반적인 메시지
}
7. 웹 애플리케이션 방화벽(WAF) 사용
- SQL Injection을 포함한 다양한 웹 공격 패턴을 감지하고 차단합니다.
# AWS WAF 규칙 예시
- name: SQLInjectionRule
priority: 1
statement:
sqli_match_statement:
field_to_match:
all_query_arguments: {}
text_transformations:
- priority: 1
type: URL_DECODE
- priority: 2
type: HTML_ENTITY_DECODE
action:
block: {}
8. 정기적인 보안 점검 및 취약점 스캔
- 코드 리뷰와 자동화된 보안 스캐너를 통해 SQL Injection 취약점을 주기적으로 점검합니다.
# OWASP ZAP을 사용한 자동 보안 스캔
zap-full-scan.py -t http://your-app.com -r zap-report.html
# SQLMap을 사용한 SQL Injection 테스트
sqlmap -u "http://your-app.com/login" --data="username=test&password=test" --dbs
안전한 코드 예시 비교
// 취약한 코드
public List<Product> searchProducts(String category, String priceRange) {
String sql = "SELECT * FROM products WHERE category = '" + category +
"' AND price " + priceRange;
// SQL Injection 위험
}
// 안전한 코드
public List<Product> searchProducts(String category, String operator, BigDecimal price) {
// 입력 검증
if (!isValidCategory(category) || !isValidOperator(operator)) {
throw new IllegalArgumentException("Invalid input parameters");
}
String sql = "SELECT * FROM products WHERE category = ? AND price " + operator + " ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, category);
stmt.setBigDecimal(2, price);
return executeQuery(stmt);
}
}
SQL Injection은 OWASP Top 10에 지속적으로 포함되는 중요한 보안 위험 요소입니다.
개발 초기부터 적절한 방어 기법을 적용하고, 보안 의식을 가지고 코드를 작성하는 것이 중요합니다.
질문 10: 데이터베이스 성능 최적화 방법에는 어떤 것들이 있나요?
모범 답변
데이터베이스 성능 최적화는 애플리케이션의 전체적인 성능과 사용자 경험에 직접적인 영향을 미치는 중요한 작업입니다.
다양한 레벨에서 최적화가 가능하며, 상황에 맞는 적절한 기법을 적용하는 것이 중요합니다.
1. 쿼리 최적화
효율적인 인덱스 사용
- WHERE, JOIN, ORDER BY, GROUP BY 절에 자주 사용되는 열에 인덱스 생성
- 복합 인덱스 생성 시 선택성이 높은 열을 앞에 배치
-- 자주 검색되는 열에 인덱스 생성
CREATE INDEX idx_users_email ON users(email);
-- 복합 인덱스 생성 (검색 + 정렬 최적화)
CREATE INDEX idx_orders_user_date ON orders(user_id, order_date);
-- 함수 기반 인덱스 (대소문자 무관 검색)
CREATE INDEX idx_products_name_upper ON products(UPPER(name));
쿼리 실행 계획 분석
- EXPLAIN (또는 DBMS별 동등한 명령)을 사용하여 쿼리 실행 계획 확인
- 테이블 스캔 대신 인덱스 스캔이 사용되는지 확인
-- 쿼리 실행 계획 확인
EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'completed';
-- MySQL에서 상세한 실행 계획
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE user_id = 123;
-- PostgreSQL에서 실제 실행 통계 포함
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 123;
필요한 열만 선택
- SELECT * 대신 필요한 열만 명시적으로 선택
-- 나쁜 예
SELECT * FROM products WHERE category_id = 5;
-- 좋은 예
SELECT id, name, price FROM products WHERE category_id = 5;
LIMIT 사용
- 결과 세트 크기를 제한하여 메모리 사용량과 전송 데이터 최소화
-- 페이징 처리로 성능 향상
SELECT * FROM logs
ORDER BY created_at DESC
LIMIT 20 OFFSET 100;
-- 최신 데이터만 조회
SELECT * FROM notifications
WHERE user_id = 123
ORDER BY created_at DESC
LIMIT 10;
JOIN 최적화
- 불필요한 JOIN 제거
- 조인 순서 최적화 (작은 결과 집합을 먼저 조인)
- 적절한 JOIN 유형 선택
-- 효율적인 JOIN 순서 (작은 테이블부터)
SELECT o.order_id, u.username, p.product_name
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
WHERE u.status = 'active'
AND o.order_date >= '2024-01-01';
2. 스키마 설계 최적화
적절한 정규화/비정규화
- 3NF 또는 BCNF까지 정규화 후, 성능 요구사항에 따라 선택적 비정규화
- 자주 함께 조회되는 데이터를 비정규화하여 JOIN 감소
-- 비정규화 예: 제품 테이블에 카테고리 이름 포함
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(100),
category_id INT,
category_name VARCHAR(50), -- 비정규화된 필드
price DECIMAL(10,2),
INDEX idx_category (category_id),
INDEX idx_price (price)
);
적절한 데이터 타입 선택
- 필요 이상으로 큰 데이터 타입 피하기
- 날짜/시간에 적합한 타입 사용
-- 나쁜 예
CREATE TABLE users (
id INT,
age VARCHAR(10), -- 숫자에 문자열 타입 사용
created_at VARCHAR(50) -- 날짜에 문자열 타입 사용
);
-- 좋은 예
CREATE TABLE users (
id INT,
age TINYINT, -- 작은 정수 타입 사용
created_at TIMESTAMP -- 날짜 타입 사용
);
파티셔닝(Partitioning)
- 대용량 테이블을 논리적인 파티션으로 분할
- 시간 기반, 범위 기반, 해시 기반 파티셔닝 활용
-- MySQL에서 날짜 기반 파티셔닝 예시
CREATE TABLE orders (
id INT,
user_id INT,
amount DECIMAL(10,2),
order_date DATE
)
PARTITION BY RANGE (YEAR(order_date)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- PostgreSQL에서 해시 파티셔닝
CREATE TABLE user_activities (
id SERIAL,
user_id INT,
activity_type VARCHAR(50),
created_at TIMESTAMP
) PARTITION BY HASH (user_id);
CREATE TABLE user_activities_0 PARTITION OF user_activities
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE user_activities_1 PARTITION OF user_activities
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
3. 인덱스 전략
선택적 인덱싱
- 선택성(고유한 값의 비율)이 높은 열에 인덱스 생성
- 카디널리티가 낮은 열(성별, 상태 등)은 단독 인덱싱 피하기
-- 좋음: 고유성이 높은 열
CREATE INDEX idx_users_email ON users(email);
-- 나쁨: 고유성이 낮은 열 (단독으로는 비효율적)
-- CREATE INDEX idx_users_gender ON users(gender); -- 값의 종류가 적음
-- 좋음: 복합 인덱스로 활용
CREATE INDEX idx_users_status_created ON users(status, created_at);
복합 인덱스 전략
- WHERE, ORDER BY, GROUP BY 절을 함께 고려하여 인덱스 설계
- 인덱스 사용 순서 고려 (WHERE 조건 > ORDER BY > GROUP BY)
-- 다음 쿼리에 최적화된 인덱스
-- SELECT * FROM orders WHERE user_id = ? AND status = ? ORDER BY created_at DESC
CREATE INDEX idx_orders_user_status_date ON orders(user_id, status, created_at DESC);
-- 범위 조건이 있는 경우 인덱스 설계
-- SELECT * FROM products WHERE category_id = ? AND price BETWEEN ? AND ?
CREATE INDEX idx_products_category_price ON products(category_id, price);
인덱스 오버헤드 관리
- 너무 많은 인덱스는 쓰기 성능 저하의 원인
- 사용되지 않는 인덱스 제거
- 인덱스 사용 통계 모니터링
-- PostgreSQL에서 인덱스 사용 통계 확인
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;
-- MySQL에서 사용되지 않는 인덱스 찾기
SELECT
object_schema,
object_name,
index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
AND count_star = 0
AND object_schema != 'mysql'
ORDER BY object_schema, object_name;
4. 캐싱 및 메모리 최적화
데이터베이스 버퍼 캐시 설정
- DBMS의 메모리 설정 최적화 (buffer pool, shared buffers 등)
-- MySQL 설정 예시 (my.cnf)
[mysqld]
innodb_buffer_pool_size = 4G # 서버 메모리의 50-80% 할당
innodb_buffer_pool_instances = 4
query_cache_size = 256M
query_cache_type = 1
# PostgreSQL 설정 예시 (postgresql.conf)
shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 256MB
maintenance_work_mem = 1GB
쿼리 캐시 활용
- 자주 실행되는 동일한 쿼리 결과를 캐싱
- 애플리케이션 레벨에서 Redis, Memcached 등 사용
// Java에서 Redis를 사용한 쿼리 결과 캐싱 예시
@Service
public class ProductService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductRepository productRepository;
public List<Product> getProductsByCategory(Long categoryId) {
String cacheKey = "products:category:" + categoryId;
// 캐시에서 먼저 조회
List<Product> cachedProducts = (List<Product>) redisTemplate.opsForValue().get(cacheKey);
if (cachedProducts != null) {
return cachedProducts;
}
// 캐시에 없으면 데이터베이스에서 조회
List<Product> products = productRepository.findByCategoryId(categoryId);
// 결과를 캐시에 저장 (1시간 TTL)
redisTemplate.opsForValue().set(cacheKey, products, 1, TimeUnit.HOURS);
return products;
}
}
연결 풀링(Connection Pooling)
- 데이터베이스 연결 생성/종료 오버헤드 감소
// HikariCP 연결 풀 설정 예시
@Configuration
public class DatabaseConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("pass");
config.setMaximumPoolSize(20); // 최대 연결 수
config.setMinimumIdle(5); // 최소 유지 연결 수
config.setConnectionTimeout(30000); // 연결 타임아웃 30초
config.setIdleTimeout(600000); // 유휴 연결 타임아웃 10분
config.setMaxLifetime(1800000); // 연결 최대 수명 30분
return new HikariDataSource(config);
}
}
5. 트랜잭션 및 락(Lock) 최적화
트랜잭션 크기 최소화
- 트랜잭션 내 작업 수 및 시간 최소화
- 긴 트랜잭션은 락 경합 증가 및 동시성 저하
// 나쁜 예: 긴 트랜잭션
@Transactional
public void processLargeDataSet(List<Data> dataList) {
for (Data data : dataList) { // 10,000개 데이터 처리
// 복잡한 비즈니스 로직
processComplexLogic(data);
dataRepository.save(data);
}
}
// 좋은 예: 배치 처리로 트랜잭션 분할
public void processLargeDataSet(List<Data> dataList) {
int batchSize = 100;
for (int i = 0; i < dataList.size(); i += batchSize) {
List<Data> batch = dataList.subList(i, Math.min(i + batchSize, dataList.size()));
processBatch(batch); // 각 배치마다 별도 트랜잭션
}
}
@Transactional
public void processBatch(List<Data> batch) {
for (Data data : batch) {
processComplexLogic(data);
dataRepository.save(data);
}
}
적절한 격리 수준 선택
- 애플리케이션 요구사항에 맞는 최소한의 격리 수준 사용
-- 읽기 전용 작업에 낮은 격리 수준 사용
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT COUNT(*) FROM orders WHERE status = 'completed';
COMMIT;
낙관적 락(Optimistic Locking) 활용
- 버전 컬럼을 사용한 충돌 감지
// JPA에서 낙관적 락 사용 예시
@Entity
public class Product {
@Id
private Long id;
private String name;
private BigDecimal price;
@Version
private Long version;
// getters, setters
}
@Service
public class ProductService {
@Transactional
public void updateProductPrice(Long productId, BigDecimal newPrice) {
try {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
product.setPrice(newPrice);
productRepository.save(product);
} catch (OptimisticLockingFailureException e) {
// 버전 충돌 시 재시도 로직
throw new ConcurrentUpdateException("상품이 다른 사용자에 의해 수정되었습니다.");
}
}
}
6. 모니터링 및 지속적 최적화
성능 메트릭 모니터링
- 쿼리 실행 시간, 캐시 히트율, I/O 사용량, 연결 수 등
- 모니터링 도구 활용 (Prometheus, Grafana, PMM 등)
# Prometheus 설정 예시
version: '3.7'
services:
mysql-exporter:
image: prom/mysqld-exporter
environment:
- DATA_SOURCE_NAME=user:password@(mysql:3306)/
ports:
- "9104:9104"
depends_on:
- mysql
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
슬로우 쿼리 분석
- 슬로우 쿼리 로그 활성화 및 주기적 검토
-- MySQL에서 슬로우 쿼리 로그 설정
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 1초 이상 걸리는 쿼리 기록
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- 슬로우 쿼리 로그 분석 (mysqldumpslow 사용)
-- mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
-- PostgreSQL에서 슬로우 쿼리 설정
-- postgresql.conf
log_min_duration_statement = 1000 -- 1초 이상 쿼리 로깅
log_statement = 'all'
log_duration = on
정기적인 유지보수
- 테이블 및 인덱스 조각화 관리 (OPTIMIZE TABLE, VACUUM 등)
- 통계 정보 업데이트
-- MySQL에서 테이블 최적화
OPTIMIZE TABLE orders, order_items, products;
-- 테이블 분석으로 통계 업데이트
ANALYZE TABLE orders, order_items, products;
-- PostgreSQL에서 테이블 정리 및 분석
VACUUM ANALYZE orders;
VACUUM ANALYZE order_items;
-- 전체 데이터베이스 통계 업데이트
ANALYZE;
7. 하드웨어 및 인프라 최적화
디스크 I/O 최적화
- SSD 활용
- RAID 구성 최적화
- 로그 파일과 데이터 파일 분리
-- MySQL에서 파일 위치 설정
[mysqld]
datadir = /data/mysql
log-bin = /logs/mysql/mysql-bin
innodb_data_home_dir = /data/mysql
innodb_log_group_home_dir = /logs/mysql
서버 사양 강화
- 충분한 RAM 확보
- 다중 CPU/코어 활용
데이터베이스 분산
- 읽기/쓰기 분리 (Read Replicas)
- 샤딩 도입
# Spring Boot에서 Master/Slave 구성 예시
spring:
datasource:
master:
url: jdbc:mysql://master.example.com:3306/mydb
username: master_user
password: master_pass
slave:
url: jdbc:mysql://slave.example.com:3306/mydb
username: slave_user
password: slave_pass
실제 사례를 통한 성능 최적화 예시
-- 최적화 전: 느린 쿼리 (5초 소요)
SELECT p.name, p.price, c.name as category_name,
COUNT(r.id) as review_count,
AVG(r.rating) as avg_rating
FROM products p
JOIN categories c ON p.category_id = c.id
LEFT JOIN reviews r ON p.id = r.product_id
WHERE p.status = 'active'
AND p.created_at >= '2024-01-01'
GROUP BY p.id, p.name, p.price, c.name
ORDER BY p.created_at DESC;
-- 최적화 후: 개선된 쿼리 (300ms 소요)
-- 1. 적절한 인덱스 생성
CREATE INDEX idx_products_status_created ON products(status, created_at);
CREATE INDEX idx_reviews_product_id ON reviews(product_id);
-- 2. 서브쿼리로 집계 분리하여 성능 향상
SELECT p.name, p.price, c.name as category_name,
COALESCE(r.review_count, 0) as review_count,
COALESCE(r.avg_rating, 0) as avg_rating
FROM products p
JOIN categories c ON p.category_id = c.id
LEFT JOIN (
SELECT product_id,
COUNT(*) as review_count,
AVG(rating) as avg_rating
FROM reviews
GROUP BY product_id
) r ON p.id = r.product_id
WHERE p.status = 'active'
AND p.created_at >= '2024-01-01'
ORDER BY p.created_at DESC
LIMIT 20;
최적화 결과
- 페이지 로딩 시간: 5초 → 300ms로 감소
- 데이터베이스 CPU 사용률: 80% → 30%로 감소
- 동시 사용자 처리 용량: 3배 증가
- 메모리 사용량: 40% 감소
데이터베이스 성능 최적화는 단일 작업이 아닌 지속적인 과정입니다.
실제 사용 패턴을 모니터링하고, 병목 현상을 식별하며, 일반적인 원칙과 함께 특정 DBMS의 특성을 고려한 최적화 전략을 수립해야 합니다.
마치며: 효과적인 데이터베이스 면접 대비 전략
데이터베이스 관련 면접은 단순히 지식을 암기하는 것보다, 개념을 깊이 이해하고 실제 상황에 적용할 수 있는 능력을 평가합니다.
다음 전략을 통해 더 효과적으로 면접을 준비할 수 있습니다:
1. 기본기 다지기
- 관계형 데이터베이스의 핵심 개념(정규화, 트랜잭션, 인덱싱 등)을 확실히 이해하세요.
- SQL 문법과 주요 명령어를 능숙하게 사용할 수 있어야 합니다.
- NoSQL 데이터베이스의 주요 유형과 사용 사례를 파악하세요.
2. 실전 경험 강조하기
- 자신이 작업한 프로젝트에서의 데이터베이스 관련 경험을 준비하세요.
- 특히 성능 최적화, 스키마 설계, 문제 해결 사례 등을 구체적으로 설명할 수 있어야 합니다.
- "어떤 문제가 있었고, 어떻게 해결했으며, 결과는 어땠는지" 형식으로 답변을 구성하세요.
3. 시스템 설계 능력 개발하기
- 간단한 애플리케이션부터 대규모 시스템까지 데이터베이스 설계를 연습하세요.
- 요구사항에 따라 적절한 데이터베이스 유형과 스키마를 선택하는 근거를 설명할 수 있어야 합니다.
- 확장성, 가용성, 일관성 등의 트레이드오프를 고려하는 습관을 들이세요.
4. 최신 트렌드 파악하기
- 클라우드 기반 데이터베이스 서비스(AWS RDS, Google Cloud SQL 등)에 대해 알아두세요.
- 분산 데이터베이스, 시계열 데이터베이스 등 특수 목적 데이터베이스의 개념을 이해하세요.
- 데이터 웨어하우스, 데이터 레이크 등 빅데이터 관련 개념을 숙지하세요.
5. 면접 전 준비하기
- 지원하는 회사가 어떤 데이터베이스 기술을 사용하는지 사전 조사하세요.
- 해당 기술에 대해 더 심도 있게 공부하고, 관련 경험을 강조할 준비를 하세요.
- 기술 블로그나 GitHub를 통해 회사의 엔지니어링 문화를 파악하세요.
📚 추천 학습 자료
📖 책
- "SQL Antipatterns" - Bill Karwin
- "Designing Data-Intensive Applications" - Martin Kleppmann
- "High Performance MySQL" - Baron Schwartz
🎓 온라인 강좌
- Coursera의 데이터베이스 관련 강좌
- Udemy의 SQL 마스터 과정
- MongoDB University
💻 연습 플랫폼
- LeetCode Database 문제
- HackerRank SQL 챌린지
- SQLBolt (대화형 SQL 튜토리얼)
📄 문서
- 각 DBMS의 공식 문서와 성능 최적화 가이드
- OWASP Top 10 보안 가이드
🎯 마무리 팁
데이터베이스 지식은 백엔드 개발자, 데이터 엔지니어, DBA 등 다양한 직무에서 핵심적인 역량입니다.
이론적 지식과 실제 적용 능력을 균형 있게 발전시키면,
기술 면접에서 자신감 있게 답변할 수 있을 뿐 아니라 실무에서도 뛰어난 성과를 낼 수 있을 것입니다.
면접은 단순히 합격을 위한 관문이 아니라, 자신의 지식과 경험을 성찰하고 발전시키는 기회로 삼으세요.
이 글에서 다룬 10가지 질문들을 깊이 이해하고 자신만의 답변을 준비한다면, 데이터베이스 관련 면접에서 좋은 결과를 얻을 수 있을 것입니다.
💡 더 깊이 있는 학습을 원한다면?
이 가이드와 함께 영상으로도 학습해보세요!
[DB면접] 데이터베이스 면접 핵심질문 1편|모범답안 & 암기노트 정리 (개발자 면접 필수 CS 기본기)
👉 유튜브 영상 보기
행운을 빕니다! 🚀
'개발자 취업가이드' 카테고리의 다른 글
시니어 개발자가 기대하는 2-3년차 개발자의 핵심 역량: 성장을 위한 실전 가이드 (0) | 2025.05.16 |
---|---|
포트폴리오에 넣으면 좋은 사이드 프로젝트 주제 20선: 개발자 취업 성공 전략 (0) | 2025.05.15 |
[2025년] 8편 - 첫 출근 전 준비 체크리스트 - 실무 적응을 위한 완벽 가이드 (0) | 2025.05.04 |
[2025년] 7편 - 신입 개발자 연봉 협상 가이드 - 처우부터 협상 전략까지 (1) | 2025.01.30 |
[2025년] 6편 - 기술 면접 프로젝트 설명 가이드 - STAR 기법부터 실전 답변까지 (0) | 2025.01.30 |