# 데이터베이스 - 설계의 중요성
- 자바나 파이썬과 같은 애플리케이션 코드는 상대적으로 수정하기 쉽다. 최신 프레임워크 및 아키텍처 패턴 덕분에 기능 개선 및 코드 리팩토링이 유연해졌다.
- 반면 데이터베이스 스키마 / 테이블 구조는 한번 잘못 만들어지면 바로 잡는 데에 큰 비용과 시간이 소요된다.
- 설계가 잘못되는 경우 다음과 같은 문제들이 발생하게 된다.
- 데이터 무결성 훼손
- 대량의 중복 데이터 발생에서 시작되어 수정 이상(Update Anomaly), 삽입 이상 (Insertion Anomaly), 삭제 이상(Deletion Anomaly)와 같은 현상들이 발생한다.
- 성능 저하
- 디스크에서 읽어들일 데이터 양이 많아져 속도가 느려진다.
- 유지보수 비용 증가
- 비즈니스 요구 사항 변화에 대해 빠른 대응이 어렵다.
- 데이터 무결성 훼손
수정, 삽입, 삭제 이상
- order 테이블에 구분없이 모든 데이터를 합쳐 관리하는 상황이다.
- 유저 정보, 주문 정보가 합쳐져있다.
- 수정 이상 (Update Anomaly)
- WHERE절을 통해 특정 주문 아이디값 데이터에 대해서만 유저 정보 수정이 이루어진 경우, 모든 유저에게 일관성있게 적용되는 것이 아닌 특정 행에만 값 수정이 반영되어 데이터 일관성이 깨지게 된다.
- 삽입 이상 (Insertion Anomaly)
- 주문을 하지 않으면 고객 정보 등록이 불가능하다.
- 불필요한 정보를 넣어야만 원하는 데이터를 저장할 수 있는 상황이다.
- 삭제 이상 (Deletion Anomaly)
- 주문정보 삭제 시 유저의 정보까지 묶여 삭제된다.
- 특정 정보를 삭제했을 뿐인데 다른 중요한 정보까지 연쇄적으로 삭제되는 현상
# 설계의 3단계 - 개념, 논리, 물리
- 개념, 논리, 물리는 데이터베이스 설계의 3단계이다.
- 개념적 설계 (Conceptual Design)
- 비즈니스 아이디어 및 요구사항을 이해하고 현실세계 정보들을 컴퓨터 세상의 언어로 번역
- 우리가 다룰 데이터가 무엇인지 (Entity), 그 데이터들이 어떤 관계를 맺고 있는지 (Relationship) 설계
- 산출물: ERD(Entity-Relationship Diagram), 사람의 눈으로 가장 이해하기 쉬운 형태의 설계도
- 논리적 설계 (Logical Design)
- 개념적 설계 밑그림을 관계형 DB 원리에 맞게 구체적인 구조로 다듬는 단계
- RDBMS에 종속되지 않는 논리적 데이터 구조 구축
- 어떤 테이블 구조로 표현할지, 데이터 중복을 막고 관계를 명확히 하기 위해 어떤 정규화를 적용할지
- 산출물: 정규화된 테이블 스키마, 엔티티를 테이블로 만들고, 각 테이블은 어떤 컬럼을 가질지, FK / PK는 어떻게 설정할지 등
- 물리적 설계 (Physical Design)
- 실제로 사용할 RDBMS의 특성에 맞게 최적화 및 구현하는 단계
- 산출물: 물리적 테이블 정의서, SQL 스크립트
- 개념적 설계 (Conceptual Design)
- 각 설계 단계마다 사용되는 용어가 다르다.
- 개념 모델
- 현실 세계 / 비즈니스 논리를 중점으로 하여 소통하기 위한 목적
- 사용 용어: 대상 - 엔티티(Entity), 특징 - 속성(Attribute), 관계(Relationship)
- 논리 모델
- 개념 모델에서 파악한 요구사항을 관계형 DB 이론에 맞게 체계적으로 구조화
- 사용 용어: 릴레이션, 속성, 튜플, 기본 키, 외래 키
- 릴레이션은 데이터베이스 모델에서 테이블을 부르는 공식적인 이름이다.
- 실무에서는 테이블, 컬럼, 행이라는 표현을 그대로 쓴다.
- 물리 모델
- 논리 모델에서 만든 설계도를 특정 DBMS에 실제로 구현할 수 있도록 구체적인 명세를 정하는 것
- 실제 성능, 저장공간, DBMS 종속 기술
- 테이블, 컬럼, 행, 데이터타입 (VARCHAR, INT), 인덱스 등
- 개념 모델
# 개념적 모델링
- 요구사항 분석 시 데이터 뼈대는 명사와 동사로부터 만들어낼 수 있다.
- 명사 (Nouns): 엔티티 또는 속성이 될 확률이 높다.
- 동사 (Verbs): 데이터들 사이의 행위나 관계가 될 확률이 높다.
- 엔티티란, 저장할 만한 가치가 있는 정보를 여러 개 가지고 있으면서, 다른 것과 명확히 구분되는 유무형의 모든 것을 말한다.
- 우리가 데이터를 저장하고 관리해야 할 대상이다.
- 유형 엔티티
- 사람, 사물, 장소와 같이 물리적 형태가 있는 대상을 의미한다.
- 회원 / 직원 / 상품 등
- 무형 엔티티
- 물리적 형태는 없지만 개념적으로 존재하는 관리 대상, 주로 사건이나 개념을 데이터로 관리할때 나타난다.
- 주문, 예약, 수강신청, 계좌 등
- 개념적 모델링 단계 용어들이 데이터베이스 물리적 테이블과는 다음과 같이 연결된다.
- 엔티티 -> 테이블
- 속성 -> 열, 컬럼
- 인스턴스 -> 행
- 좋은 엔티티 특성은 다음과 같다.
- 업무 관련성 (Business Relevance)
- 해당 업무에 필요하고 관리해야 하는 정보
- 식별 가능해야 한다. (Uniquely Identifiable)
- 엔티티에 속하는 각 데이터는 서로 명확하게 구분될 수 있어야 한다. (인스턴스 ,행)
- 두 개 이상의 정보를 가진다. (Has Attributes)
- 엔티티는 관리할만한 가치가 있는 여러 정보 / 속성들을 가진다.
- 어떤 대상이 단 하나의 정보만 가지고 있다면 다른 엔티티의 속성일 확률이 높다.
- 인스턴스의 집합이다.
- 엔티티는 개념적인 틀이고, 그 틀에 따라 만들어진 실제 데이터 각각을 인스턴스라 부른다.
- 엔티티: 회원 / 인스턴스: 회원1, 회원2, ..
- 다른 엔티티와 관계를 맺는다.
- 엔티티는 홀로 존재하지 않고 다른 엔티티와 관계를 맺는다.
- 업무 관련성 (Business Relevance)
# 엔티티 분류
- 엔티티(데이터 덩어리) 성격과 역할을 제대로 파악하고 구분하는 과정이 엔티티 분류이다.
- 파악한 데이터 본질을 기반으로 테이블 구조, 키 설계, 관계 설정, 성능 최적화 전략까지 설계의 큰 그림을 그릴 수 있다.
- 엔티티를 구분하는 주요 기준은 존재 형태, 생성 시점 및 역할, 존재 종속성(독립성)이다.
- 존재 형태를 기준으로 한 엔티티 분류는 다음과 같다.
- 유형 엔티티
- 물리적 형태를 가지고 있어 눈으로 보거나 만질 수 있는 실체를 표현하는 엔티티
- 업무로부터 식별이 용이하다. 업무란 현실세계에서 존재하는 비즈니스 활동을 통칭한다.
- 사원, 학생, 교수, 상품, 강의실, 지점 등
- 상대적으로 안정적, 지속적으로 활용되는 정보
- 개념 엔티티
- 물리적 형태는 없지만 업무적으로 관리해야 할 중요한 개념이나 아이디어를 표현하는 엔티티
- 부서, 조직, 계좌, 과목 등
- 유형 엔티티와 마찬가지로 상대적으로 안정적
- 사건 엔티티
- 업무 프로세스가 진행됨에 따라 발생하는 특정 행위나 사건을 표현하는 엔티티
- 특정 시점에 발생하며, 비즈니스 활동 결과를 기록하는 역할
- 업무 수행에 따라 발생하는 행위를 기록
- 시간의 흐름에 따라 인스턴스가 계속 발생
- 주문, 계약, 결제 등
- 업무 프로세스가 진행됨에 따라 발생하는 특정 행위나 사건을 표현하는 엔티티
- 엔티티를 어떻게 분류하고 구성하는지 살펴보면 시스템 최종 목적과 정체성을 엿볼 수 있다.
- 회원 / 상품은 유형 / 개념 엔티티다.
- 주문은 주문이라는 사건이 발생할때마다 생성되는 사건 엔티티다.
- 유형, 개념엔티티는 상대적으로 정적이다. 그에 반해 사건 엔티티는 사건이 발생할때마다 인스턴스가 생성되기 때문에 더 동적이다.
- 위와 같이 데이터의 성격과 증가 추이를 미리 파악하면 테이블 설계 시 여러 전략들을 고려해볼 수 있다.
- 인덱스 전략: 데이터가 많아진 경우 인덱스를 추가하여 조회 성능을 개선
- 데이터 파티셔닝 / 아카이빙: 월별 연도별 기준으로 물리적 분리
- 파티셔닝: 최신 데이터는 더 작은 영역에서 조회하도록 하여 성능 개선
- 아카이빙: 사용빈도가 극히 낮은 데이터를 별도의 스토리지로 옮겨 운영 테이블을 작게 유지
- 유형 엔티티
- 역할 및 발생 시점에 따른 분류는 다음과 같다.
- 기본 엔티티(Fundamental / Key Entity)
- 다른 엔티티에 의해 생성되지 않고 독립적으로 존재할 수 있는 핵심 엔티티
- 다른 엔티티의 부모 역할을 수행
- 독립적인 생성 가능
- 자신만의 고유 주식별자를 가짐
- 회원, 상품, 사원 등
- 중심 엔티티 (Main / Center Entity)
- 기본 엔티티로부터 파생되어 생성 / 업무에서 중심적인 역할을 수행하는 엔티티
- 기본 엔티티간의 관계에서 발생하는 핵심적인 비즈니스 트랜잭션을 나타내는 경우가 많음
- 데이터 발생량이 많고, 다른 엔티티와의 관계를 통해 행위 엔티티를 생성하는 허브 역할
- 주문, 계약, 청구 등
- 행위 엔티티 (Action / Behavioral Entity)
- 둘 이상의 부모 엔티티로부터 발생하며, 업무가 흘러가면서 생성되는 상세 정보를 기록하는 엔티티
- 내용이 자주 변경되거나 지속적으로 증가하는 특징을 가짐.
- 주문 이력, 결제 내역, 로그 등
- 기본 엔티티(Fundamental / Key Entity)
- 다른 엔티티와의 관계 속에서 자신의 존재 여부가 결정되는지 여부에 따라 강한 엔티티와 약한 엔티티로 분류할 수 있다.
- 강한 엔티티 (Strong Entity)
- 다른 엔티티 존재 여부와 상관없이 독립적으로 존재할 수 있는 엔티티
- 자신만의 고유 식별자를 가짐. (PK)
- 다른 엔티티 존재에 의존하지 않는다.
- 약한 엔티티 (Weak Entity)
- 다른 엔티티가 존재하지 않으면 독립적으로 존재할 수 없는 존재 종속적 엔티티
- 부모 종속적 표현을 강제하기 위해 소유 엔티티 주식별자와 자신의 속성을 결합한 것을 주식별자로 삼는다. (Composite Primary Key)라 부른다.
- 위와 같은 관계를 논리적 모델링 단계에서 식별 관계라고 한다.
- 강한 엔티티 (Strong Entity)
- 엔티티 분류 후에는 엔티티 속성들 중 식별자를 반드시 선택해야 한다.
- 식별자는 각 엔티티 인스턴스를 다른 인스턴스와 유일하게 구별해주는 이름표 역할을 한다. DB 테이블에서는 PK로 구현된다.
# 카디널리티
- 관계 정의 시에는 더 상세한 규칙들을 마련해야 한다.
- 한 명의 회원이 여러 주문을 할 수 있는지?
- 주문 없이 회원만 존재 가능한지?
- 관계의 규칙 정의시 카디널리티(Cardinality)와 참여도(Optionality)가 핵심 요소로 여겨진다.
- 카디널리티는 한 엔티티 인스턴스가 다른 엔티티 인스턴스와 몇개나 관계를 맺을 수 있는지를 나타내는 수량 제약이다.
- 카디널리티 관계는 네 종류가 있다.
- 일대일(1:1)
- 주문과 배송
- 일대다(1:N)
- 회원과 주문
- 다대일(N:1)
- 주문과 회원
- 다대다(M:N)
- 주문과 상품 (한 주문에 여러 상품이 포함, 한 상품에 여러 주문이 들어옴)
- 일대일(1:1)
- 엔티티 인스턴스 하나가 다른 엔티티 인스턴스와 맺을 수 있는 최대숫자를 기준으로 표현한다.
- 카디널리티 관계는 네 종류가 있다.
- 참여도는 한 엔티티의 인스턴스가 관계에 반드시 참여해야 하는지, 참여하지 않을 수도 있는지를 결정한다.
- 회원 입장에서 주문은 필수적이지 않다. (Optional)
- 주문 입장에서 회원은 필수적이다. (Mandatory)
# 관계 표현과 외래 키
- FK같은 외래 키는 구현 단계에서 사용되는 개념이다.
- 개념적 모델링 단계에서는 NoSQL에서도 사용 가능할 수 있도록, FK와 같은 특정 구현 방식을 명시하지 않는다.
- 엔티티간의 관계는 관계선(Relationship)으로 표현하며, 외래키 표현을 위해 엔티티 내에 외래키에 해당하는 컬럼은 포함시키지 않는다.
- 선으로 이어주게 된다.
- 이후 논리적 모델링 단계에서 관계선이 외래키로 표현된다.
# ERD(Entity-Relationship Diagram)
- ERD는 복잡한 데이터 구조를 시각적으로 표현하여 관계자들이 시스템 청사진을 한눈에 파악하고 소통할 수 있게 도와주는 도구이다.
- ERD 표기법은 피터 첸 표기법과 까마귀발 표기법이 있다.
- 피터 첸 표기법
- 엔티티는 사각형, 관계는 마름모, 속성은 타원으로 표현하는 전통적 방식이다.
- 실무에서는 사용하지 않는다.
- 까마귀발 표기법
- 실무에서 사용되는 사실상의 표준이다.
- 엔티티는 사각형 상자로 표현한다.
- 관계는 두 엔티티를 잇는 직선으로 표현한다.
- 선의 양 끝에 기호를 붙여 카디널리티와 참여도를 동시에 표현한다.
|: 1 One-<-: 발가락 3개의 모양이며, Many를 표현한다.- 0: Zero
- 위 기호들을 조합하여 관계를 표현한다.
- Zero와 섞어서 선택적 데이터인지 (0개 데이터도 가능한지) 필수적 데이터인지 (1개 이상 반드시 데이터를 가져야 함) 표현 가능하다.
- 까마귀발 표기법에서 가장 중요한 원칙은 한 엔티티 끝쪽에 있는 기호는 반대편 엔티티 규칙을 설명한다는 것이다.
- 피터 첸 표기법
# 다대다 관계의 해결
- 다대다 관계는 개념적으로 타당하지만, 해결 불가능한 문제가 존재한다.
- 우선 물리적으로 구현 불가능하다는 점이 있다.
- 개념적으로 그리는 것은 쉽지만 실제 데이터베이스 테이블 구조로 옮길 수 없다.
- 주문 아이디를 PK로, 상품 아이디를 FK로 설정한다.
- 주문 아이디 101에 대해 상품 아이디 1001, 1002가 묶인다고 가정하자.
- 이때 상품 아이디 1002 값을 갖는 인스턴스가 다른 주문아이디도 개념상으로는 가질 수 있다.
- 이 경우 상품 아이디 1002에 대해서는 어떤 주문 아이디를 가져야 하는 지에 대해 구현이 불가능하다.
- 한 주문 당 상품 최대 갯수를 정하고 남는 공간은 NULL로 표현한다.
- 최대 갯수가 늘어나거나 줄어드는 경우 큰 문제가 발생
- 불필요한 NULL 공간 차지
- 조회성능 저하
- 한 컬럼 내에 구분자로 값 욱여넣기
- 조회의 어려움
- 데이터 수정 및 삭제가 어려움
- 데이터 원자성 위반
- 개념적으로 그리는 것은 쉽지만 실제 데이터베이스 테이블 구조로 옮길 수 없다.
- 둘째로, 관계에 속한 데이터를 저장할 장소가 없다.
- 주문 이후 처리 과정에서 중간에 상품 가격이 변경되었다고 가정해보자.
- 상품 테이블에는 변경된 가격 값이 반영된다.
- 주문 당시 변경 전 가격 값을 원본 테이블에서 참조하게 되면 주문 이후 시점에도 변경된 값을 참조해버리게 된다.
- 변경 전 가격 값을 테이블에 저장하는 것은 주문 테이블의 목적성에 부합하지 않는다.
- 이러한 문제를 해결하는 방법은 관계를 엔티티로 승격하는 것이다.
- M:N 관계를 연관 엔티티(Associative Entity)로 바꿔야 한다.
- 주문 <-> 상품 사이에 주문 항목이라는 연관 엔티티를 넣어, 1:N / N:1 관계를 추가한다.
- 하나의 주문은 여러 주문 항목 리스트와 관계를 맺을 수 있다.
- 하나의 상품은 여러 주문 항목들과 관계를 맺을 수 있다.
- M:N 관계는 거의 100% 연관 엔티티로 해소된다.
- 두 엔티티 사이에 관리해야 할 정보가 있는지를 파악하는 것이 중요하다.
- 하나의 주문에 여러 상품인데, 주문당시 가격을 스냅샷하는것과 각 주문에서의 주문 상품마다 몇개를 주문했는지 독립적으로 관리해야 한다.
# 논리적 모델링 (Logical Modeling)
- 논리적 모델링은 개념적 모델링에서 ERD 등으로 만든 청사진을 관계형 데이터베이스 구조에 맞게 변환하는 과정이다.
- 엔티티 -> 테이블
- 속성 -> 컬럼
- PK 및 FK 정의
- 특정 DBMS에 속하지 않게 순수한 관계형 데이터베이스 모델링을 하는 것
# 키 (Key)
- 논리적 모델링에서 중요한 것은 키이다.
- 키는 데이터 행을 유일하게 식별할 수 있게 해주는 장치이다.
- 데이터 조회 용도 외에, 테이블 간 관계, 데이터 오입력 / 중복 등을 해결해주는 무결성 제약 조건의 역할도 갖는다.
- 기본 키(PK)
- 테이블 모든 행을 유일하게 식별하는 대표 키이다.
- NOT NULL / UNIQUE / 불변성 세 조건을 반드시 만족해야 한다.
- 후보 키(Candidate Key)
- PK가 될 수 있는 후보들이다.
- 유일성: 모든 행을 서로 구분할 수 있어야함.
- 최소성: 행을 유일하게 식별하는 데에 최소한의 컬럼만 포함해야 한다.
- 두 조건을 만족해야 한다.
- 대체 키(Alternate Key)
- 후보 키들 중 기본 키로 선택되지 않은 모든 키들
- 외래 키(Foreign Key)
- 테이블간의 관계를 연결하는 역할
- 다른 테이블의 PK를 참조
- 자연 키(Natural Key)
- 비즈니스 로직 안에서 자연스럽게 발생하는 의미를 갖는 데이터이다.
- 대한민국 국민 테이블 -> 주민등록번호
- 도서 테이블 -> ISBN
- 자연 키는 불변성의 규칙을 만족하기가 어렵다.
- 대리 키(Surrogate Key), 인조 키(Artificial Key)
- 대리키는 절대 변하지 않는 키값이다.
- 비즈니스와 상관없이 시스템이 자동으로 생성해주는 값이다.
- autoincrement 값이나, UUID를 사용한다.
- 자연키는 PK로서 역할을 하는 것이 아닌, UNIQUE 처리하여 의미있는 데이터에 대해 중복을 허용하지 않는 제약만 걸어두는 것으로도 충분하다.
- 자연 키를 PK로 사용하면 쿼리가 직관적이고 단순해진다는 장점은 있다.
- 반면 자연키를 참조하는 외부 테이블의 크기가 자연키 값만큼 추가적인 공간을 차지해야 하기 때문에 공간 낭비 문제가 있다.
- 대리 키는 쿼리 시 추가 조인작업 및 인덱스가 필요하다.
- 트레이드 오프가 있긴 하지만 현대에는 대리키 사용을 적극 권장한다.
- 복합 키(Composite Key)
- 복합키는 둘 이상의 컬럼을 묶어 유일성을 확보하는 방식이다.
PRIMARY KEY (col1, col2, col3)와 같이 제약을 건다.- 변경 가능성, 외래키 참조 복잡성 및 크기 증가 등의 문제는 여전히 발생한다.
- 복합키 역시 UNIQUE KEY 제약 조건만 걸어두고 관리하는 것이 바람직하다.
- 연관 엔티티에서의 아이디 역시 대리 키 PK에, M:N 관계를 갖는 두 테이블의 아이디를 하나로 묶어 UNIQUE 처리를 하는 것이 현대 방법론이다.
# 논리적 모델링 - 참여도, 일대다 관계
- 데이터베이스 관계는 절대 단방향이 아니다. 애초에, 관계형 데이터베이스 관계에는 방향이 없다.
- 회원과 팀이 있을때 OOP 기반에서는 팀 내의 회원은 참조하여 조회 가능하지만 회원이 어떤 팀에 속했는지 조회하는 것은 어렵다.
- 반면 데이터베이스는 PK -> FK / FK -> PK 방향으로의 조회 모두 가능하다.
SELECT
t.name
FROM member m
JOIN team t ON m.team_id = t.team_id
WHERE m.name = '잡스';
SELECT
t.name
FROM team t
JOIN member m ON m.team_id = t.team_id
WHERE m.name = '잡스'
- 따라서 두 테이블이 관계를 맺기 위해서는 둘 중 하나에 FK를 두면 되는데, 어디에 이를 두느냐가 많은 설계 정책에 영향을 준다.
# 관계의 핵심 요소 - 카디널리티와 참여도
- 논리적 모델링에서는 카디널리티와 참여도라는 두 핵심적 속성을 갖는다.
- 카디널리티: 한 테이블 행이 다른 테이블 행과 몇개나 연결될 수 있는지 (1:1, 1:N..)
- 참여도: 필수 참여인지, 선택 참여인지를 나타낸다.
- 일대다 관계에서 N쪽 테이블은 NULL 제약조건으로 쉽게 구현이 가능하다.
- member 테이블의 team_id FK 컬럼을 NULL 허용으로 구현: 선택적 참여
- member 테이블의 team_id FK 컬럼을 NOT NULL로 구현: 필수적 참여
- 즉, 일대다 기준으로 N 테이블 인스턴스가 부모 테이블 참조에 대한 참여도를 NULL 제약조건 하나로 구현 가능하다.
- 일대다 관계에서 1쪽 테이블은 구현에 한계가 존재한다.
- team 테이블에 인스턴스 하나를 추가만 하면 member 테이블에서 참조를 하지 않는 이상 제로 참여도부터 자연스럽게 시작된다.
- 반면 필수적 참여도는 FK 컬럼이 team 테이블에 없기 때문에 DB 제약으로 구현이 불가능하다.
- 규칙을 강제할 컬럼이 명확히 없는 상태이다.
- 이 경우는 애플리케이션 계층에서 로직으로 해결하게 된다.
- 팀 인스턴스 생성과 멤버 생성을 하나의 트랜잭션으로 처리한다.
- 트리거 등을 활용하여 DB계층에서 직접 해결 가능하게 해볼 순 있겠지만, 복잡성이 매우 올라간다.
# 외래키의 위치
- 외래키는 일대다 관계에서 다(N)쪽에 위치해야 한다.
- 1쪽에 위치하는 경우 1쪽의 테이블의 PK가 중복되는 문제가 발생한다.
- 또한, 컬럼마다의 값은 단 하나만 존재해야 하므로 separator 기반으로 데이터를 욱여넣는 것은 원자성을 위반하게 된다.
- 일대다 조인과 다대일 조인 시 데이터 뻥튀기 현상을 이해해야 한다.
- 다대일 조인
- FK -> PK로 조인
- 뻥튀기가 발생하지 않는다.
- 일대다 조인
- PK -> FK로 조인
- 뻥튀기가 발생한다.
- 다대일 조인
# 일대일 관계
- 일대일 관계는 하나의 테이블로 합치는 게 효율적인 경우가 많지만, 그럼에도 이러한 관계를 맺는 데에는 이유가 있다.
- 두 엔티티가 항상 함께 조회되는 케이스 (User - User Profile)
- N쪽 테이블이 필수 참여일때 그냥 합쳐도 됨
- 일대일 관계가 의미있는 경우
- 성능 최적화
- 모든 부가정보가 하나의 테이블에 포함되어 있다면 불필요한 데이터 조회에 의한 성능 저하가 발생한다.
- 보안 강화
- 민감정보는 테이블로 분리하고, 테이블 접근권한을 별도로 부여한다.
- 선택적 참여 여부 표현
- 1대1에서 한쪽이 선택적 참여인 경우 테이블 분리가 필요하다.
- 비즈니스 모델
- 엔티티 사이의 성격이 아주 상이할때
- 관리 주체나 라이프사이클이 다른 데이터를 관리해야 할때 분리한다.
- 성능 최적화
- 일대일 관계 구현시에는 반드시 FK쪽에 UNIQUE 제약조건을 걸어두어야 한다.
- 일대일 관계에서 FK 위치는 어느 곳이던 문법적으로는 문제가 되지 않는다.
- 일반적인 경우, 보조 테이블에 FK를 둔다.
- 비즈니스 요구사항에 의해 보조 테이블이 N 테이블로 변경될 수도 있다.
- 일반적인 경우, 보조 테이블에 FK를 둔다.
CREATE TABLE board (
board_id BIGINT NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content TEXT NULL,
PRIMARY KEY (board_id)
);
CREATE TABLE upload_file (
upload_file_id BIGINT NOT NULL AUTO_INCREMENT,
board_id BIGINT NOT NULL,
upload_file_name VARCHAR(255) NOT NULL,
PRIMARY KEY (upload_file_id),
UNIQUE KEY up_board_id (board_id),
CONSTRAINT fk_upload_file_board FOREIGN KEY (board_id) REFERENCES board (board_id)
);
- 위와 같은 일대일 테이블 구성에서 일대다로 관계 변경에 대한 요구사항이 생겼다.
- 이 경우 기존 FK 제약조건을 제거하고, 유니크 인덱스를 지운다. 이후 FK를 다시 추가하면 된다.
- 일대일 관계에서 조회 성능이 매우 중요한 경우 주테이블에 FK를 두어 데이터 NULL 여부 검증을 빠르게 할 수 있다.
- 불필요한 조인도 줄일 수 있다. (인덱스 설계가 더 나을 수는 있음)
# 다대다 관계
- 위에서 배웠듯 다대다 관계를 두 테이블로만 구현할때 원자성, PK 유일성 등의 여러 제약사항들을 위반한다.
- 관계형 데이터베이스가 외래 키 하나로 단일 행을 가리키는 방식으로 관계를 맺기 때문이다.
- 이를 해결하기 위해 연결 테이블 (Junction Table) 개념을 사용한다.
- 개념적 모델링에서는 연관 엔티티라고 부른다.
CREATE TABLE order_product (
order_product_id BIGINT NOT NULL AUTO_INCREMENT,
order_id BIGINT NOT NULL ,
product_id BIGINT NOT NULL,
PRIMARY KEY (order_product_id),
CONSTRAINT uq_order_product UNIQUE (order_id, product_id),
CONSTRAINT fk_order_product_orders FOREIGN KEY (order_id) REFERENCES orders (order_id),
CONSTRAINT fk_order_product_product FOREIGN KEY (product_id) REFERENCES products (product_id)
);
- 양쪽으로 참조할 각 테이블들의 아이디값의 묶음은 UNIQUE 처리를 한다.
- 양쪽 각 테이블을 참조하여 FK 제약조건을 걸어둔다.
- M:N 관계인 데이터를 조회하기 위해서는 연결 테이블을 거쳐 조인을 해야한다.
# 식별 관계와 비식별 관계
- 각 관계의 예시를 보자.
- 식별 관계: 아파트 동호수
- 아파트 호수는 아파트의 동에 강력하게 의존한다.
- 자식 식별자가 부모 식별자를 포함해야만 완전해지는 관계이다.
- 비식별 관계: 사원번호
- 부서 소속, 직급 등은 식별자로 사용되지 않는다.
- 부모와의 관계가 자식 식별에 필수적이지 않는 경우가 비식별 관계이다.
- 식별 관계: 아파트 동호수
- 식별 관계: 부모가 자식을 식별하는 데에 사용되는 관계
- 부모 테이블의 PK를 받아 자식 테이블의 PK + FK로 사용한다.
- 부모 엔티티 없이 식별 자체가 불가능하면 강한 관계에 해당한다.
- 개념적 모델링에서는 이러한 엔티티를 약한 엔티티라 한다.
- 비식별 관계: 부모가 자식을 식별하는 데에 사용되지 않는 관계
- 부모 테이블의 PK를 받아 자식테이블의 일반 컬럼 + FK로 사용한다.
- 자식 테이블도 고유 PK를 갖는다.
- 스스로 존재 가능하고, 자신만의 고유 식별자를 갖는다면 부모와 자식은 약한 관계이다.
- 이러한 엔티티는 강한 엔티티라 불린다.
- 각 관계별 예시
- 비식별 관계: 게시글과 댓글에서 각 댓글의 아이디를 유일하게 관리하는 경우
- WHERE를 통해 직접 참조 및 수정이 가능
- 식별 관계: 댓글이 게시글 아이디를 물려받아 자신의 식별자의 일부로 사용
- 게시글 아이디가 없으면 댓글이 존재할 수 없는 것이 표현됨
- 비식별 관계: 게시글과 댓글에서 각 댓글의 아이디를 유일하게 관리하는 경우
- 식별 관계는 기본적으로 강한 결합이며, 이는 실무적으로 유연성과 확장성을 크게 떨어트린다.
- 자식, 손자 테이블로 확장되어 테이블 계층이 생기는 경우가 존재한다.
- 이때 PK 컬럼의 전파 / PK 오염 문제가 발생한다.
- 자식 테이블의 PK가 (부모_PK, 본인_PK)이기 때문이다.
- 현대 DB 설계에서는 대부분 비식별 관계를 사용한다.
- 비식별 관계 + 자식의 PK로는 대리 키를 사용하는 것이 일반적이다.
# 정규화
- 정규화란 데이터 중복을 최소화하고, 데이터 일관성을 보장하며, 데이터 모델을 더 유연하게 만들기 위한 과정이다.
- 모든 데이터 컬럼들이 하나의 테이블에서 관리된다면 갱신 이상, 삽입 이상, 삭제 이상과 같은 여러 문제들이 발생한다.
# 함수 종속성 (Functional Dependency)
- 정규화 이해를 위한 기초 개념으로 함수 종속성이 있다.
- 함수 종속성이란 컬럼 값들이 다른 컬럼의 값을 유일하게 결정하는 관계를 의미한다.
X -> Y와 같이 표기하며, X가 Y를 함수적으로 결정한다고 읽는다.- X는 결정자, Y는 종속자이다.
- X가 Y를 유일하게 결정한다는 뜻이다.
member_id -> member_name, 이러한 함수 종속성을 예시로 들 수 있다.- 특정 멤버 아이디는 특정 멤버의 이름을 유일하게 결정한다.
# 제1 정규형
- 테이블의 모든 컬럼이 원자적인 값만을 가져야 한다.
- separator를 활용하여 한 컬럼에 여러 값을 욱여넣으면 안된다.
# 제2 정규형
- 테이블 PK가
(order_id, product_id)처럼 복합 키로 사용되고 있다고 가정해보자.- 이때 몇몇 컬럼들은 PK의 일부에만 종속되는 경우가 존재할 수 있다.
order_id -> member_id, member_id는 order_id에만 종속되며 product_id와는 관계가 없다.order_id -> ordered_at등
- 위와 같이 기본 키의 일부에만 종속되는 컬럼이 존재하는 것을 부분 함수 종속이라(Partial Functional Dependency)한다.
- 제2 정규형의 정의는 다음과 같다.
- 제1 정규형을 만족해야 한다.
- 테이블의 모든 컬럼이 기본 키에 대해 완전 함수 종속이어야 한다.
- 부분 함수 종속이 없어야 한다.
- 부분 함수 종속에 해당하는 컬럼들을 모아 별도의 테이블로 분리해야 한다.
# 제3 정규형
- 기본 키가 아닌 컬럼이 다른 컬럼을 결정하는 관계를 이행적 함수 종속이라(Transitive Functional Dependency)한다.
- A -> B이고 B -> C일때 A -> C가 성립하는 관계
- A -> B라는 의미는 A의 값을 알때 B가 결정된다는 것을 의미한다.
- 이러한 이행적 함수 종속이 존재하지 않아야 한다.
- 결정자가 후보키이거나 후보키의 일부(prime attribute)인 경우에는 이행적 함수 종속에 해당되지 않는다.
- PK -> 일반컬럼 (가능)
- 후보키 -> 일반컬럼 (가능)
- PK -> 후보키 (가능)
- 후보키 -> PK (가능)
- prime attribute -> 일반컬럼 (가능) ← 추가
- 인과관계가 아닌 값이 하나로 특정되는지 여부를 표현하는 것
| 학생ID (PK) | 학생명 | 학과코드 | 학과명 |
|---|---|---|---|
| 1 | 김철수 | CS | 컴퓨터공학과 |
| 2 | 이영희 | CS | 컴퓨터공학과 |
| 3 | 박민준 | ME | 기계공학과 |
- 위 테이블에는 학과코드 → 학과명이라는 이행적 함수 종속이 존재한다.
- 학생ID(PK)가 학과코드를 결정하고, 학과코드가 다시 학과명을 결정하는 이행 구조다.
- 학과코드는 후보키가 아닌 일반 컬럼임에도 학과명을 결정하고 있어 3NF를 위반한다.
- 제3 정규형을 적용하는 경우 학생 테이블과 학과 테이블이 분리된다.
# BCNF(Boyce-Codd Normal Form) 정규형
- 기본키가 아니더라도 어떤 컬럼이 다른 컬럼을 결정한다면, 해당 결정자는 반드시 후보 키여야 한다는 규칙이다.
- 복합 후보키가 존재할때 복합 후보키의 일부가 다른 일반 컬럼의 값을 결정할 수 없다.
- 제3 정규형은 위의 허점을 가지고 있다.
- BCNF는 이러한 후보키의 일부가 결정자로서 역할을 하는 것을 절대 허용하지 않는다.
- 모든 결정자는 반드시 후보키 자체여야 한다.
# 물리적 모델링
- 물리적 모델링은 논리적 모델을 특정 DBMS 특성과 성능을 고려하여 구체적인 물리적 스키마로 변환하는 과정이다.
- 성능과 DBMS에 초점을 맞추어 모델링을 진행한다.
- 물리적 테이블, 컬럼, 데이터 타입 정의, 인덱스 생성, 파티셔닝 설계, 스토리지 엔진, 프로시저 등 실질적 구현 진행
- 물리적 모델링의 프로세스는 다음과 같다.
- 테이블과 컬럼 변환
- 논리적 모델의 한글명을 영문으로 변경한다.
- 데이터 타입 정의
- 각 컬럼에 적합한 데이터 타입을 선택한다.
- 저장공간과 성능에 직접적인 영향을 준다.
- 제약 조건 설정
- PK / FK / NOT NULL 등 데이터 무결성 보장을 위한 제약조건을 설정한다.
- 인덱스 설계
- 데이터 조회 성능 극대화를 위해 인덱스를 설계한다.
- 역정규화 및 성능 튜닝
- 필요에 따라 정규화 원칙을 위배하여 테이블을 통합하거나 중복 데이터를 추가하는 역정규화를 실시한다.
- 파티셔닝, 샤딩 등 적용
- 대용량 테이블의 경우 데이터를 분할하여 저장하는 기법을 사용
- 뷰, 프로시저, 함수, 트리거 등 사용
- 테이블과 컬럼 변환
영문명 작성 컨벤션
- 테이블의 PK 컬럼은 테이블명_id로 명명한다.
- FK는 참조하는 PK 이름을 그대로 사용한다.
- 날짜 / 시간 컬럼은 접미사로 용도를 명확히 한다.
_at,_datetime등
- 불리언 타입 컬럼은
is_,has_접두사를 사용한다. - 컬럼은 단수 명사를 사용한다.
- 축약어는 이미 널리 사용되는 용어가 아니면 권장되지 않는다.
- 테이블명은 단수 / 복수 둘다 사용해도 괜찮지만 일관성있게 적용해야 한다.
VARCHAR와 메모리 사용량
- VARCHAR는 가변길이 문자열 타입이다.
- GROUP BY / ORDER BY와 같은 작업들을 수행할때, MySQL은 디스크 데이터를 메모리로 가져와 중간 결과물을 저장할 임시 테이블을 만든다.
- 이때 만들어지는 임시 컬럼 크기가 VARCHAR의 최대 길이를 따라간다.
INT vs BIGINT
- 초기 테이블 정의 시 테이블의 PK로는 BIGINT 타입이 더 권장된다.
- 전체 데이터 1억건 정도가 되어야 INT와 BIGINT에 의한 용량 차이가 400MB 수준이 된다.
- 그전까지는 용량의 차이가 미미하기 때문에 타입 차이에 의한 용량 손해보다, 미래 확장성 및 관리 용이성 등의 장점이 더 크다.
DATE
- 실무에서는
created_at,updated_at컬럼은 기본으로 사용된다.- 문제 추적 및 데이터 분석에 유용하게 사용된다.
- 개발자가
NOW()등의 함수를 직접 호출하는 것은 휴먼 에러의 여지가 존재한다.DEFAULT CURRENT_TIMESTAMP,ON UPDATE CURRENT_TIMESTAMP제약조건을 걸어두면 데이터 생성 시 명시적인 함수 호출이 아니더라도 현 시각으로 값이 업데이트 된다.
CREATE TABLE board_sample (
board_id BIGINT NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content TEXT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (board_id)
);
created_at은 데이터베이스에 물리적으로 생성된 시각,ordered_at과 같은 컬럼은 비즈니스 행위가 이루어진 시점을 기록한다.
# 역정규화(Denormalization)
- 역정규화는 데이터 조회 성능을 향상시키기 위해 의도적으로 데이터 모델 정규화 원칙을 위반하여 데이터 중복을 허용하는 프로세스이다.
- 정규화를 따른다고 해서 항상 데이터베이스 설계가 완벽해지는 것은 아니다.
- 정규화를 수행한 경우 테이블이 잘게 쪼개지게 된다.
- 데이터가 여럿이 쌓이게 되는 경우 JOIN의 빈도가 늘어나고, 이 경우 시스템 성능을 저하시키게 된다.
- 역정규화는 데이터 일관성 및 정합성을 일부 희생하고, 조회 속도를 얻는 트레이드오프 관계에 있다.
- 역정규화에는 여러 기법들이 존재한다.
- 중복 컬럼 추가
- 조인 연산 후 가져올 컬럼 데이터를 그대로 복사하여 가져온다.
- 데이터 불일치 가능성이 커지기 때문에 트리거와 같은 데이터 동기화 로직들을 추가 구현해야 한다.
- 파생 컬럼 추가
- 조회 시점에 SUM, COUNT와 같은 연산이 자주 이루어지는 경우 해당 계산 결과를 미리 컬럼에 저장해둔다.
- 쓰기 작업에 부하를 주며, 값 업데이트 시 갱신하는 로직을 구현해야 한다.
- 테이블 통합 및 분할
- 항상 함께 조회되는 테이블을 합쳐 JOIN을 제거한다.
- 한 테이블의 컬럼이 대량이고, 일부 컬럼만 사용하는 경우 수직분할을 수행한다. 컬럼 4개가 있을때 자주 사용되는 컬럼 전용으로 2개, 2개로 분할하는 방식을 예로 들 수 있다.
- 중복 컬럼 추가
- 역정규화로 인한 데이터 불일치 문제는 다음 방식들로 개선 가능하다.
- 애플리케이션 로직: 개발자가 책임
- 데이터베이스 트리거: 특정 테이블에 INSERT, DELETE, UPDATE 등의 이벤트 발생 시 정의해둔 쿼리 수행
- 배치 작업: 데이터 불일치 문제를 트래픽이 없는 시간대에 몰아서 처리
- 반드시 논리적 모델링 과정에서 정규화를 모두 마친 뒤, 물리적 모델 구현 후 운영 과정에서 발생하는 이슈에 따라 필요한 부분에 한해 역정규화를 적용해야 한다.