정규화·반정규화와 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정규형: 키의 일부에만 매달린 칼럼을 뺀다
위 테이블의 키는 (학번, 과목코드) 복합키다. 그런데 학생명은 학번만으로, 과목명과 교수명은 과목코드만으로 정해진다. 이게 부분 종속이다. 키 전체가 아니라 일부에만 종속된 칼럼은 각자의 테이블로 뺀다.
student (학번, 학생명)course (과목코드, 과목명, 교수명)enrollment (학번, 과목코드, 성적) -- 성적만 키 전체에 종속3정규형: 키가 아닌 칼럼끼리의 연쇄를 끊는다
course에 교수명 옆으로 교수의 소속 학과를 추가했다고 하자. 과목코드 → 교수명 → 소속학과로 이어지는 이행 종속이 생긴다. 교수가 학과를 옮기면 그 교수가 맡은 모든 과목 row를 고쳐야 하니 갱신 이상이 되살아난다. 키가 아닌 칼럼이 다른 키 아닌 칼럼을 결정하면 그것도 뺀다.
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정규형은 이행 종속을 없앤다. 키 전체에, 오직 키에만.
- 반정규화는 측정한 병목에만, 동기화 책임을 지고 일부러 한다.
- 로직은 기본적으로 애플리케이션에 두고, 프로시저는 대량 배치와 왕복 병목에만 쓴다.