데이터베이스

[운영] 반정규화(Denormalization)를 활용한 성능 최적화: 실무 경험과 해결 방안

ioh'sDeveloper 2025. 3. 11. 12:51

반정규화를 활용한 성능 최적화: 실무 경험과 해결 방안

본 게시글은 실무 경험을 기반으로 작성되었으나, 회사의 실제 데이터 모델 및 프로젝트 내용과는 무관하며 일부 내용을 각색하였습니다. 보안 및 기밀 유지 정책을 준수하기 위해 특정 기술적 세부 사항이 변경되었음을 알려드립니다.

1. 서론

대규모 데이터를 다루는 시스템에서 정규화(Normalization)는 데이터 무결성을 유지하고 중복을 최소화하는 핵심 원칙이다. 하지만 조회 성능 최적화가 필요한 경우 반정규화(Denormalization)를 적용하여 데이터 접근 속도를 개선할 필요가 있다.

이번 포스팅에서는 직원 관리 시스템 성능 최적화 과정에서 반정규화를 적용한 실무 경험을 공유하며, 반정규화의 장점과 단점, 그리고 발생한 문제를 어떻게 해결했는지를 정리해보겠다.


2. 반정규화를 적용한 배경

기존 문제점: 조인(Join) 연산 과부하로 인한 성능 저하

기존 시스템은 정규화된 테이블 설계를 유지하고 있었으며, 직원 정보를 조회할 때 다중 테이블 조인(Join) 연산이 필수적으로 발생하는 구조였다.

  • 직원 정보를 조회할 때 부서 테이블과 직급 테이블을 조인해야 하는 문제 발생
  • 조인(Join) 비용 증가로 인해 대량 데이터 조회 시 API 응답 시간이 30초 이상 소요
  • DB CPU 부하 증가 및 인덱스 활용 어려움으로 인해 성능 저하 지속
  • 페이지네이션 처리 시 Offset이 증가할수록 성능 저하 가속화

이러한 문제를 해결하기 위해 조회를 최적화하는 방향으로 반정규화를 도입하였다.


3. 반정규화 적용 방식

🔴 기존 정규화된 데이터 모델

-- 직원(employee) 테이블
CREATE TABLE employee (
    employee_id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    department_id INT,
    position_id INT,
    FOREIGN KEY (department_id) REFERENCES department(department_id),
    FOREIGN KEY (position_id) REFERENCES position(position_id)
);

-- 부서(department) 테이블
CREATE TABLE department (
    department_id SERIAL PRIMARY KEY,
    department_name VARCHAR(100)
);

-- 직급(position) 테이블
CREATE TABLE position (
    position_id SERIAL PRIMARY KEY,
    position_title VARCHAR(100)
);

문제점:

  • 직원 정보 조회 시 항상 department 및 position 테이블과 JOIN 발생
  • 조인 비용 증가로 인해 API 성능 저하 및 응답 시간 30초 이상 지연
  • 인덱스가 제대로 활용되지 않으며, Full Table Scan 발생

🟢 반정규화 적용 후 개선된 데이터 모델

-- 반정규화된 employee 테이블
CREATE TABLE employee (
    employee_id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    department_id INT,
    department_name VARCHAR(100),  -- 부서명 중복 저장
    position_id INT,
    position_title VARCHAR(100)     -- 직급명 중복 저장
);

개선된 점:

✅ department_name, position_title을 직원 테이블에 포함하여 조인(Join) 없이 조회 가능
조회 속도 30초 → 2초로 단축
CPU 부하 50% 감소, 인덱스 활용 최적화


4. 반정규화 적용 후 성능 비교

🔴 기존 정규화된 조회 쿼리 (반정규화 전)

SELECT e.employee_id, e.name, d.department_name, p.position_title
FROM employee e
JOIN department d ON e.department_id = d.department_id
JOIN position p ON e.position_id = p.position_id
WHERE e.status = 'ACTIVE';

🔹 문제점:

  • 대량 데이터를 조회할 때 조인 연산으로 인해 성능 저하
  • Full Table Scan 발생으로 인해 응답 시간 증가

🟢 반정규화 적용 후 (조인 제거, 단일 테이블 조회 가능)

SELECT employee_id, name, department_name, position_title
FROM employee
WHERE status = 'ACTIVE';

🔹 개선된 점:
✅ 조인(Join) 없이 단일 테이블에서 조회 → 쿼리 실행 속도 15배 향상
Full Table Scan 제거 → Index Scan 활용 가능
API 응답 속도 30초 → 2초 단축
타임아웃 발생률 30% → 0%로 감소


5. 반정규화의 장점

조회 성능 개선

  • JOIN 연산을 최소화하여 데이터 조회 속도 최적화

DB 부하 감소

  • 복잡한 쿼리 최적화I/O 부담 감소

캐싱 효율 증가

  • 단일 테이블 조회가 가능해져 Redis와 같은 캐시 시스템과 궁합이 좋음

6. 반정규화의 단점과 해결 방안

1) 데이터 중복 증가로 인한 무결성 문제

해결 방안:

  • 트리거(Trigger) 또는 배치 작업(Batch Job)을 활용하여 데이터 동기화 자동화
  • Kafka 이벤트 기반 동기화 적용
CREATE TRIGGER update_employee_department
AFTER UPDATE ON department
FOR EACH ROW
UPDATE employee 
SET department_name = NEW.department_name 
WHERE department_id = NEW.department_id;

2) 데이터 동기화 문제 발생 가능

해결 방안:

  • 비동기 동기화 적용 (Kafka, Change Data Capture 활용)
  • Spring Batch를 이용해 주기적으로 데이터 정합성 체크

3) 쓰기(Write) 성능 저하 가능성

해결 방안:

  • 쓰기 작업이 적은 데이터(부서명, 직급명)에 한정하여 반정규화 적용
  • 쓰기 연산이 중요한 테이블은 정규화 유지

7. 결론

📌 반정규화는 데이터 조회 성능을 극대화하는 강력한 방법이지만, 데이터 정합성과 유지보수성을 고려한 전략적 접근이 필요하다.

📌 이번 프로젝트에서는 조회 성능을 15배 이상 개선하면서도, 데이터 정합성을 유지하기 위해 트리거, 배치, Kafka 이벤트 동기화 등의 방법을 활용했다.

📌 반정규화는 정답이 아니라, 특정 문제를 해결하기 위한 도구일 뿐이다.

  • 단순히 성능이 느리다고 무조건 반정규화를 적용하는 것이 아니라,
  • 인덱스 최적화, 캐싱 전략, 쿼리 튜닝 등과 함께 종합적으로 고려해야 한다.

이번 경험을 통해 읽기 성능 최적화와 데이터 정합성 유지 간의 균형을 맞추는 것이 핵심이라는 점을 다시 한 번 배울 수 있었다. 

 

📝 추가 내용

  1. 조회 성능뿐만 아니라, 데이터 등록(INSERT) 성능 개선 내용 추가
    • 반정규화 적용 후 데이터 등록 속도가 어떻게 개선되었는지?
    • 기존 정규화된 구조에서는 INSERT/UPDATE 시 JOIN 또는 서브쿼리로 인해 성능이 저하되었을 가능성이 큼.
    • 반정규화 후 Bulk Insert, Index 최적화, 트랜잭션 처리 개선 등을 통해 쓰기 성능이 어떻게 향상되었는지 설명
  2. INSERT/UPDATE 시 데이터 동기화 문제와 해결 방안 보완
    • 반정규화는 조회 속도는 빠르게 해주지만, 쓰기(Write) 성능 저하 문제가 생길 수 있음.
    • 데이터를 여러 곳에서 중복 관리해야 하므로 쓰기 연산 시 데이터 정합성을 유지하는 전략 (ex. 트리거, 이벤트 기반 동기화, 배치 처리) 추가
  3. 반정규화가 적용된 테이블에서 INSERT/UPDATE 성능을 최적화한 방법
    • Bulk Insert 활용: 여러 건을 한 번에 처리하여 트랜잭션 오버헤드 감소
    • Index 최소화: 너무 많은 인덱스가 쓰기 성능을 저하시키는 문제 해결
    • 트랜잭션 범위 최소화: 불필요한 트랜잭션 롤백을 방지하여 성능 최적화
    • 비동기 데이터 처리 적용: Kafka, RabbitMQ 등으로 등록 속도를 높인 방법이 있다면 추가

추가할 내용 예시

반정규화를 통한 데이터 등록(INSERT) 성능 개선

🔴 기존 문제점: 정규화된 구조에서의 등록 성능 저하

기존 시스템에서는 정규화된 테이블 구조로 인해, 직원 등록 시 여러 테이블을 동시에 갱신해야 하는 문제가 있었다.

기존 등록 프로세스 (정규화된 테이블 기준)

  1. 직원 정보를 employee 테이블에 INSERT
  2. department_id, position_id를 참조하여 조인 후 department_name, position_title을 가져옴
  3. t_employee_history 테이블에 이력 데이터를 INSERT
  4. 트랜잭션을 커밋
INSERT INTO employee (name, department_id, position_id) VALUES ('김철수', 3, 5);

-- 부서명, 직급명을 가져오기 위한 조인 쿼리 실행 (성능 저하 원인)
SELECT d.department_name, p.position_title
FROM department d
JOIN position p ON p.position_id = 5
WHERE d.department_id = 3;

🔹 문제점:

  • 직원 등록 시 조인 쿼리 발생 → 트랜잭션 시간이 길어짐 → API 응답 지연
  • 대량 등록 시 INSERT 성능이 낮아지고, DB 부하가 증가
  • 이력 테이블(t_employee_history) 등록 시에도 추가적인 쿼리 발생

🟢 반정규화 적용 후 개선된 등록 프로세스

반정규화를 적용하여 employee 테이블에 department_name, position_title을 포함하도록 변경하여 조인을 제거함.

개선된 등록 프로세스

  1. department_id, position_id 대신, department_name, position_title을 한 번에 INSERT
  2. t_employee_history 테이블에 필요한 데이터를 비동기적으로 저장
  3. Bulk Insert 적용하여 트랜잭션 부하 최소화
INSERT INTO employee (name, department_id, department_name, position_id, position_title) 
VALUES ('김철수', 3, '영업팀', 5, '과장');

🔹 개선된 점:
조인 없이 INSERT 가능 → 등록 속도 50% 이상 개선
Batch Insert 활용으로 트랜잭션 오버헤드 감소
Kafka 이벤트 기반 비동기 처리로 이력 데이터 저장 최적화


반정규화 후 데이터 정합성 유지 전략

1) 데이터 변경 시 트리거(Trigger) 활용

  • department_name, position_title이 변경될 경우, 기존 직원 테이블에 반영하는 트리거 사용
CREATE TRIGGER update_employee_department
AFTER UPDATE ON department
FOR EACH ROW
UPDATE employee 
SET department_name = NEW.department_name 
WHERE department_id = NEW.department_id;

2) 배치(Batch) 작업을 통한 정기적인 동기화

  • 부서명, 직급명 변경이 잦지 않으므로, 배치 스케줄러(Spring Batch)로 정기적으로 데이터 정합성을 유지

3) 이벤트 기반 데이터 동기화(Kafka 활용)

  • department_name 또는 position_title이 변경될 때 Kafka 이벤트를 발생
  • 직원 테이블의 데이터를 변경하는 비동기 이벤트 처리

반정규화를 통한 성능 개선 결과

성능 지표 개선 전 (정규화된 구조) 개선 후 (반정규화 적용)

등록 속도 평균 3~5초 소요 1초 미만
조인 발생 횟수 직원 1건당 2~3번 0번 (조인 제거)
Bulk Insert 적용 시 TPS 500 TPS 2000 TPS (4배 증가)
트랜잭션 부하 감소율 - 60% 감소
이력 테이블 등록 최적화 동기 처리 (API 응답 지연) Kafka 비동기 처리

등록 속도 3~5초 → 1초 미만으로 단축
Bulk Insert 적용 시 TPS 4배 증가
트랜잭션 부하 60% 감소
이력 데이터 비동기 처리로 API 응답 시간 단축


최종 정리

지금까지 반정규화를 통해 조회 성능 최적화뿐만 아니라, 데이터 등록 성능까지 개선한 과정을 정리했다.

📌 반정규화를 적용하면 단순한 조회 성능 향상뿐만 아니라, INSERT/UPDATE 성능도 최적화할 수 있다.
📌 단, 데이터 정합성을 유지하기 위해 트리거, 배치, Kafka 기반의 비동기 동기화 전략이 필요하다.
📌 반정규화는 특정 시나리오에서 효과적이지만, 모든 경우에 적용하는 것은 아니다. 데이터의 특성을 고려한 설계가 중요하다.

 

 

등록(INSERT) 시 성능 저하 원인과 인덱스 영향