정규화와 비정규화, 어디까지 나눌 것인가

2026. 10. 27. 13:00·IT
728x90
반응형

데이터베이스 설계를 배울 때는 정규화를 하라고 배운다. 그런데 실무에서 성능 문제를 만나면 "비정규화하라"는 조언을 듣는다. 둘은 반대 방향인데 어느 쪽이 맞는가. 이 글은 정규화가 무엇을 막아주는지, 그리고 그것을 포기해도 되는 조건이 무엇인지를 정리한다.

정규화가 막는 것은 모순이다

정규화의 목적은 저장 공간 절약이 아니다. 같은 사실이 여러 곳에 적히는 것을 막는 것이다.

주문 테이블에 고객 이름을 직접 넣었다고 하자. 고객이 이름을 바꾸면 그 고객의 모든 주문 행을 고쳐야 한다. 하나라도 빠뜨리면 같은 고객인데 이름이 다른 데이터가 생긴다. 어느 쪽이 맞는지 알 수 없다.

이걸 이상 현상(anomaly)이라고 부르고 셋으로 나눈다.

갱신 이상. 위 경우처럼 일부만 고쳐져 모순이 생긴다. 삽입 이상. 아직 주문한 적 없는 고객을 등록할 수 없다. 주문 행이 있어야만 고객 정보가 존재하기 때문이다. 삭제 이상. 마지막 주문을 지우면 고객 정보까지 사라진다.

정규화는 사실을 한 곳에만 적어서 이 셋을 구조적으로 없앤다. 고객 이름은 고객 테이블에만 있고, 주문은 고객 ID로 참조한다.

3정규형까지가 실무 기준

정규형은 여러 단계가 있지만 실무에서는 대체로 3정규형(3NF)까지 맞추면 충분하다.

간단히 말하면 이렇다. 모든 컬럼이 기본 키에 의존하고(2NF), 키가 아닌 컬럼끼리 의존하지 않아야 한다(3NF). 예를 들어 주문 테이블에 우편번호와 도시가 함께 있으면, 도시는 우편번호에 의존하므로 3NF 위반이다.

BCNF 이상은 학술적으로는 의미가 있지만 실무에서 문제가 되는 경우가 드물다. 3NF까지 맞춰 놓고 실제 문제가 생기면 그때 더 보면 된다.

비정규화는 측정한 뒤에 한다

정규화된 스키마는 조인이 많아진다. 조인이 늘면 느려진다는 것이 비정규화의 근거다. 그런데 여기에는 전제가 붙는다.

조인 자체는 생각보다 싸다. 인덱스가 제대로 걸린 조인은 밀리초 단위로 끝난다. 느린 원인이 조인이 아니라 인덱스 부재나 N+1인 경우가 훨씬 많다. 비정규화하기 전에 실행 계획부터 봐야 한다.

그럼에도 비정규화가 정당한 경우가 있다.

과거 시점의 값을 보존해야 할 때. 주문 시점의 상품 가격은 정규화 관점에서는 상품 테이블을 참조해야 하지만, 그러면 가격이 바뀔 때 과거 주문 금액까지 바뀐다. 주문 행에 당시 가격을 복사해 두는 것이 맞다. 이건 중복이 아니라 다른 사실이다. 현재 가격과 주문 당시 가격은 별개의 정보다.

읽기가 압도적으로 많고 조인이 깊을 때. 목록 화면에서 다섯 테이블을 조인해야 한다면, 자주 쓰는 값을 복사해 두는 것이 실용적일 수 있다.

집계 값을 매번 계산하기 비쌀 때. 게시글의 댓글 수를 매번 COUNT하는 대신 카운터 컬럼을 둔다.

비정규화의 대가를 어떻게 감당하나

복사해 둔 값은 원본이 바뀔 때 같이 갱신돼야 한다. 이걸 어떻게 보장할지가 설계의 핵심이다.

같은 트랜잭션에서 함께 갱신. 가장 확실하다. 댓글을 추가할 때 카운터도 같은 트랜잭션에서 올린다. 다만 경합이 심해질 수 있다.

데이터베이스 트리거. 애플리케이션이 잊어도 데이터베이스가 보장한다. 다만 로직이 코드 밖에 숨어서 디버깅이 어렵다.

비동기 갱신. 이벤트를 발행하고 나중에 맞춘다. 성능은 좋지만 잠시 어긋난 상태가 생긴다. 그 시간 동안 틀린 값이 보여도 괜찮은지 판단해야 한다.

주기적 재계산. 배치로 정합성을 맞춘다. 위 방법들과 함께 쓰는 안전망으로 유용하다. 어긋난 데이터가 얼마나 있는지 세어보는 것만으로도 문제를 조기에 발견한다.

어느 방법을 쓰든 어긋날 수 있다는 것을 전제로 설계해야 한다. 정합성 검증 쿼리를 만들어 두고 주기적으로 돌리는 것을 권한다.

순서를 지키면 후회가 적다

정리하면 내 기준은 이렇다.

먼저 정규화한다. 설계 단계에서는 3NF를 목표로 한다. 정규화된 스키마는 요구사항이 바뀔 때 유연하다. 비정규화된 스키마를 나중에 정규화하는 것은 훨씬 어렵다.

느려지면 원인을 측정한다. 실행 계획을 보고 인덱스와 쿼리 패턴을 먼저 고친다. 대부분 여기서 해결된다.

그래도 안 되면 비정규화한다. 이때도 어디를 왜 비정규화했는지 주석과 문서에 남긴다. 나중에 보는 사람이 "이거 실수 아닌가" 하고 고치지 않도록.

갱신 책임을 명시한다. 복사된 값을 누가 언제 갱신하는지 코드 한 곳에 모은다. 여러 곳에서 제각각 갱신하면 언젠가 어긋난다.

마지막으로 하나. 요즘은 읽기 전용 복제본이나 검색 엔진, 전용 집계 테이블처럼 원본 스키마를 건드리지 않고 읽기를 빠르게 하는 선택지가 많다. 목록 조회가 느리다고 바로 주 테이블을 비정규화하기보다, 조회 전용 뷰를 따로 만드는 편이 나은 경우가 많다. 쓰기 모델과 읽기 모델을 나누면 각자 최적의 모양을 가질 수 있다.

참고 자료

  • Database normalization — PostgreSQL Wiki — 흔한 스키마 설계 실수 모음
  • PostgreSQL: Using EXPLAIN — 비정규화 전에 원인을 측정하는 도구
  • MySQL: Optimizing Database Structure — 스키마 설계가 성능에 미치는 영향
728x90
반응형

'IT' 카테고리의 다른 글

HTTP 상태 코드와 오류 응답 설계  (0) 2026.10.26
알림 피로를 줄이는 SLO 기반 모니터링  (0) 2026.10.25
환경 변수와 시크릿 관리, 어디까지 해야 하나  (0) 2026.10.24
메시지 큐를 언제 써야 하고 언제 쓰면 안 되나  (0) 2026.10.23
트랜잭션 격리 수준과 데드락 줄이기  (0) 2026.10.22
'IT' 카테고리의 다른 글
  • HTTP 상태 코드와 오류 응답 설계
  • 알림 피로를 줄이는 SLO 기반 모니터링
  • 환경 변수와 시크릿 관리, 어디까지 해야 하나
  • 메시지 큐를 언제 써야 하고 언제 쓰면 안 되나
밍글링글링
밍글링글링
mingling - 밍글링, 밍글밍글링. 코드와 어우러지다. IT/ 프로그래밍/소스
    반응형
    250x250
  • 밍글링글링
    mingling
    밍글링글링
  • 전체
    오늘
    어제
    • 밍글링글링 (463)
      • Flutter (2)
      • 일상생활 (8)
        • 리뷰 (1)
        • 생활정보 (4)
        • 맛집 (0)
        • 여행 (0)
        • 모든정보 (3)
      • JAVA (126)
        • 개념 (6)
        • 예제 (115)
        • Exception (2)
      • C (1)
        • C (1)
        • C++ (0)
        • C# (0)
      • JS (29)
        • JavaScript (18)
        • JQuery (5)
        • AJax (0)
        • NODE.JS (6)
        • Angular.JS 2.0 (0)
      • WEB (87)
        • HTML (6)
        • CSS (61)
        • JSP (20)
        • JSTL (0)
      • FrameWork (8)
        • Spring (8)
        • BootStrap (0)
        • MyBATIS (0)
        • JUnit (0)
      • 외부 라이브러리 (5)
      • 공유 소스 관리 (5)
        • Git (5)
        • SVN (0)
      • 빅데이터 프로그래밍 (37)
        • Python (37)
        • R Programming (0)
      • DB (7)
        • ORACLE (0)
        • MySql (6)
      • Development Tools (7)
        • StarUML (0)
        • eXERD (0)
        • Eclipse (4)
      • SKILL (6)
        • Migration (0)
        • Security (6)
      • MicroSoft (0)
        • Excel (0)
        • Word (0)
      • Android (0)
      • Server (21)
        • Ubuntu (5)
        • Linux (15)
      • IOS (0)
      • XML (0)
      • 미디어 (0)
      • 공지사항 (3)
      • NETWORK (1)
      • 게임 (4)
        • 피파 (1)
        • 리니지M (0)
        • 배틀그라운드 (1)
        • 듀랑고 (2)
      • 세상 이슈 (6)
      • 일렉트론 (0)
      • 대회 소식 (4)
      • 업무 (2)
      • Express, Vue (6)
      • docker (11)
      • svelte (3)
      • 블록체인 (1)
      • IT (66)
      • Rust (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    ubuntu
    css perspective
    SSL 인증서
    nginx ssl 설정
    러스트
    nginx
    servlet class
    proxy pass
    자바 생성자
    java casting
    docker
    AI 코딩 에이전트
    nginx ssl 적용
    Java Array
    프런트엔드
    css tb
    자바 exception
    css float
    API 설계
    리눅스 설치
    Node
    오류
    VS Code 팁
    gitlab 설치
    ssl 인증서 발급
    svelte
    vue cli
    자바 클래스
    제미나이
    mysql db
    React Compiler
    자바 배열
    css transition
    티스토리 자동화
    에디터 팁
    Extension Bisect
    lang rust
    vscode
    자바 객체 지향
    rust linux
    spring java
    Rust lang
    클론코딩
    AI 반도체
    자바 for문
    vue 설치
    jsp parameter
    jsp include
    브라우저 자동화
    css table
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
밍글링글링
정규화와 비정규화, 어디까지 나눌 것인가
상단으로

티스토리툴바