본문으로 건너뛰기

정규화·반정규화와 ORM vs 프로시저 — DB 설계 정리 (DB 총정리 3/4)

DB 총정리 셋째 편은 설계 이야기다. 앞의 두 주제(정규화와 반정규화)가 테이블을 어떻게 쪼갤지에 대한 것이라면, 마지막 주제(ORM과 Stored Procedure)는 비즈니스 로직을 어디에 둘지에 대한 것이다. 둘 다 정답이 하나로 정해진 문제가 아니라 맞바꿈의 문제라는 게 이번 편의 핵심이다.

정규화를 왜 하나: 중복이 만드는 이상 현상 세 가지

정규화하지 않은 테이블 하나로 시작하자. 수강 신청 정보를 한 테이블에 다 넣었다.

학번 학생명 과목코드 과목명 교수명 성적
1001 김지호 CS101 DB개론 박교수 A
1001 김지호 CS202 운영체제 이교수 B+
1002 이서연 CS101 DB개론 박교수 A-

“DB개론은 박교수 담당”이라는 사실이 수강생 수만큼 반복해서 저장된다. 이 중복이 세 가지 사고를 낸다.

  • 갱신 이상: 과목명이 바뀌면 그 과목의 모든 row를 고쳐야 한다. 하나라도 빠뜨리면 데이터끼리 모순된다.
  • 삽입 이상: 아직 수강생이 없는 새 과목은 등록할 방법이 없다. 학번이 키의 일부라 비워 둘 수도 없다.
  • 삭제 이상: 마지막 수강생이 수강을 취소하면 과목 정보까지 함께 사라진다.

정규화는 이 중복을 함수 종속(functional dependency)을 기준으로 분해해서 하나의 사실이 딱 한 곳에만 저장되게 만드는 과정이다. “X를 알면 Y가 정해진다(X → Y)“는 관계를 따라 테이블을 쪼갠다.

1·2·3정규형

1정규형: 칸 하나에 값 하나

모든 칼럼이 원자값이어야 한다. 연락처 = "010-1111, 010-2222"처럼 한 칸에 여러 값을 쉼표로 넣는 순간 위반이다. 그 칸으로는 검색도 인덱스도 제대로 쓸 수 없으니, 반복되는 값은 row로 풀거나 별도 테이블로 뺀다.

2정규형: 키의 일부에만 매달린 칼럼을 뺀다

위 테이블의 키는 (학번, 과목코드) 복합키다. 그런데 학생명은 학번만으로, 과목명과 교수명은 과목코드만으로 정해진다. 이게 부분 종속이다. 키 전체가 아니라 일부에만 종속된 칼럼은 각자의 테이블로 뺀다.

enrollment — 한 테이블에 다학번 · 과목코드 · 학생명과목명 · 교수명 · 성적키의 일부에만 매달린 칼럼을 떼어 내면 (2정규형)student학번학생명course과목코드과목명교수명enrollment학번과목코드성적굵은 글씨 = 키 · 같은 색 = 그 키로 정해지는 칼럼
성적만 키 전체(학번, 과목코드)에 종속된다. 나머지는 자기를 정하는 키의 테이블로 간다
2정규형으로 나눈 결과
student (학번, 학생명)
course (과목코드, 과목명, 교수명)
enrollment (학번, 과목코드, 성적) -- 성적만 키 전체에 종속

3정규형: 키가 아닌 칼럼끼리의 연쇄를 끊는다

course에 교수명 옆으로 교수의 소속 학과를 추가했다고 하자. 과목코드 → 교수명 → 소속학과로 이어지는 이행 종속이 생긴다. 교수가 학과를 옮기면 그 교수가 맡은 모든 과목 row를 고쳐야 하니 갱신 이상이 되살아난다. 키가 아닌 칼럼이 다른 키 아닌 칼럼을 결정하면 그것도 뺀다.

3정규형으로 나눈 결과
course (과목코드, 과목명, 교수ID)
professor (교수ID, 교수명, 소속학과)

반정규화: 중복을 알고 되산다

정규화의 청구서는 조회할 때 날아온다. 테이블을 쪼갠 만큼 JOIN이 는다. 주문 목록 화면 하나를 그리는 데 테이블 다섯 개를 조인해야 하고, 트래픽이 많은 서비스에서 그게 병목이 되면 그때 반정규화를 꺼낸다.

  • 칼럼 중복: 주문(order)에 상품명을 복사해 둔다. 사실 주문 당시의 상품명과 가격은 상품 테이블의 현재 값과 다른 사실이라, 복사해 두는 게 원래 맞는 경우도 많다.
  • 집계 칼럼: 게시글마다 comment_count를 미리 들고 있어서 COUNT와 조인을 없앤다.
  • 테이블 통합과 요약 테이블: 자주 같이 읽는 테이블을 합치거나 통계용 테이블을 따로 둔다.

대가는 정확히 정규화가 막아 주던 것들이다. 중복이 생기니 고칠 때 여러 곳을 함께 고쳐야 하고, 그 동기화는 이제 애플리케이션의 책임이다. 집계 칼럼이라면 이렇게 된다.

댓글을 쓸 때마다 두 곳을 함께 고친다
START TRANSACTION;
INSERT INTO comment (post_id, body) VALUES (42, '...');
UPDATE post SET comment_count = comment_count + 1 WHERE id = 42;
COMMIT; -- 하나만 반영되면 숫자가 어긋난다

두 문장을 한 트랜잭션으로 묶는 이유는 2편의 원자성 그대로다. 그래서 순서가 중요하다. 먼저 정규화로 설계하고, 측정해서 확인한 병목에만 반정규화한다. 처음부터 조회하기 편하라고 뭉쳐 둔 테이블은 반정규화가 아니라 그냥 정규화를 안 한 것이다.

ORM vs Stored Procedure: 로직을 어디에 둘까

설계의 마지막 질문이다. 비즈니스 로직은 애플리케이션 코드(ORM)에 둘 수도 있고, DB 안의 프로시저로 둘 수도 있다.

ORM (JPA, Prisma 등)

  • 객체로 생각하고 SQL은 생성된다
  • 버전 관리, 테스트, 코드 리뷰가 애플리케이션 파이프라인에 그대로 들어간다
  • DBMS를 바꾸기가 상대적으로 쉽다
  • 대신 생성된 SQL을 모르면 N+1 같은 함정에 빠지고, 복잡한 쿼리에는 한계가 있다

Stored Procedure

  • 로직이 DB 안에서 돈다
  • 데이터를 옮기지 않고 대량 처리하고, 네트워크 왕복이 적다
  • 대신 버전 관리, 테스트, 디버깅이 어렵고 DBMS에 묶인다
  • CPU 부하가 DB에 몰린다. DB는 가장 비싸고 수평 확장이 어려운 자원이다

예전 SI와 금융권에서 프로시저 중심 개발이 많았던 데는 이유가 있다. 네트워크가 느리고 애플리케이션 서버가 빈약하던 시절에는 DB에서 다 하는 게 빨랐다. 지금은 반대다. 애플리케이션 서버는 수평 확장이 싸고, DB는 비싸다. 그래서 기본값은 로직을 애플리케이션에 두는 것이다.

그래도 프로시저가 이기는 경우가 있다.

  • 수백만 row를 읽어 가공한 뒤 다시 쓰는 배치성 작업. 데이터를 애플리케이션까지 끌고 오는 것 자체가 낭비다.
  • 한 트랜잭션에 여러 문장을 묶어야 해서 왕복 횟수가 성능을 좌우하는 경우.
  • 여러 시스템이 같은 DB를 함께 써서 DB 수준에서 규칙을 강제해야 하는 레거시 환경.

정리

  • 정규화는 함수 종속을 따라 분해해서 사실 하나를 한 곳에만 둔다. 이상 현상 세 가지를 막는다.
  • 2정규형은 부분 종속, 3정규형은 이행 종속을 없앤다. 키 전체에, 오직 키에만.
  • 반정규화는 측정한 병목에만, 동기화 책임을 지고 일부러 한다.
  • 로직은 기본적으로 애플리케이션에 두고, 프로시저는 대량 배치와 왕복 병목에만 쓴다.
GitHub에서 고치기
esc

최근 글

↑↓ 이동↵ 열기검색 페이지에서 보기 ›