# 데이터베이스 - 설계의 중요성

  • 자바나 파이썬과 같은 애플리케이션 코드는 상대적으로 수정하기 쉽다. 최신 프레임워크 및 아키텍처 패턴 덕분에 기능 개선 및 코드 리팩토링이 유연해졌다.
  • 반면 데이터베이스 스키마 / 테이블 구조는 한번 잘못 만들어지면 바로 잡는 데에 큰 비용과 시간이 소요된다.
  • 설계가 잘못되는 경우 다음과 같은 문제들이 발생하게 된다.
    1. 데이터 무결성 훼손
      • 대량의 중복 데이터 발생에서 시작되어 수정 이상(Update Anomaly), 삽입 이상 (Insertion Anomaly), 삭제 이상(Deletion Anomaly)와 같은 현상들이 발생한다.
    2. 성능 저하
      • 디스크에서 읽어들일 데이터 양이 많아져 속도가 느려진다.
    3. 유지보수 비용 증가
      • 비즈니스 요구 사항 변화에 대해 빠른 대응이 어렵다.

수정, 삽입, 삭제 이상

  • order 테이블에 구분없이 모든 데이터를 합쳐 관리하는 상황이다.
    • 유저 정보, 주문 정보가 합쳐져있다.
  • 수정 이상 (Update Anomaly)
    • WHERE절을 통해 특정 주문 아이디값 데이터에 대해서만 유저 정보 수정이 이루어진 경우, 모든 유저에게 일관성있게 적용되는 것이 아닌 특정 행에만 값 수정이 반영되어 데이터 일관성이 깨지게 된다.
  • 삽입 이상 (Insertion Anomaly)
    • 주문을 하지 않으면 고객 정보 등록이 불가능하다.
    • 불필요한 정보를 넣어야만 원하는 데이터를 저장할 수 있는 상황이다.
  • 삭제 이상 (Deletion Anomaly)
    • 주문정보 삭제 시 유저의 정보까지 묶여 삭제된다.
    • 특정 정보를 삭제했을 뿐인데 다른 중요한 정보까지 연쇄적으로 삭제되는 현상

# 설계의 3단계 - 개념, 논리, 물리

  • 개념, 논리, 물리는 데이터베이스 설계의 3단계이다.
    1. 개념적 설계 (Conceptual Design)
      • 비즈니스 아이디어 및 요구사항을 이해하고 현실세계 정보들을 컴퓨터 세상의 언어로 번역
      • 우리가 다룰 데이터가 무엇인지 (Entity), 그 데이터들이 어떤 관계를 맺고 있는지 (Relationship) 설계
      • 산출물: ERD(Entity-Relationship Diagram), 사람의 눈으로 가장 이해하기 쉬운 형태의 설계도
    2. 논리적 설계 (Logical Design)
      • 개념적 설계 밑그림을 관계형 DB 원리에 맞게 구체적인 구조로 다듬는 단계
      • RDBMS에 종속되지 않는 논리적 데이터 구조 구축
      • 어떤 테이블 구조로 표현할지, 데이터 중복을 막고 관계를 명확히 하기 위해 어떤 정규화를 적용할지
      • 산출물: 정규화된 테이블 스키마, 엔티티를 테이블로 만들고, 각 테이블은 어떤 컬럼을 가질지, FK / PK는 어떻게 설정할지 등
    3. 물리적 설계 (Physical Design)
      • 실제로 사용할 RDBMS의 특성에 맞게 최적화 및 구현하는 단계
      • 산출물: 물리적 테이블 정의서, SQL 스크립트
  • 각 설계 단계마다 사용되는 용어가 다르다.
    1. 개념 모델
      • 현실 세계 / 비즈니스 논리를 중점으로 하여 소통하기 위한 목적
      • 사용 용어: 대상 - 엔티티(Entity), 특징 - 속성(Attribute), 관계(Relationship)
    2. 논리 모델
      • 개념 모델에서 파악한 요구사항을 관계형 DB 이론에 맞게 체계적으로 구조화
      • 사용 용어: 릴레이션, 속성, 튜플, 기본 키, 외래 키
        • 릴레이션은 데이터베이스 모델에서 테이블을 부르는 공식적인 이름이다.
        • 실무에서는 테이블, 컬럼, 행이라는 표현을 그대로 쓴다.
    3. 물리 모델
      • 논리 모델에서 만든 설계도를 특정 DBMS에 실제로 구현할 수 있도록 구체적인 명세를 정하는 것
      • 실제 성능, 저장공간, DBMS 종속 기술
      • 테이블, 컬럼, 행, 데이터타입 (VARCHAR, INT), 인덱스 등

# 개념적 모델링

  • 요구사항 분석 시 데이터 뼈대는 명사와 동사로부터 만들어낼 수 있다.
    • 명사 (Nouns): 엔티티 또는 속성이 될 확률이 높다.
    • 동사 (Verbs): 데이터들 사이의 행위나 관계가 될 확률이 높다.
  • 엔티티란, 저장할 만한 가치가 있는 정보를 여러 개 가지고 있으면서, 다른 것과 명확히 구분되는 유무형의 모든 것을 말한다.
    • 우리가 데이터를 저장하고 관리해야 할 대상이다.
  • 유형 엔티티
    • 사람, 사물, 장소와 같이 물리적 형태가 있는 대상을 의미한다.
    • 회원 / 직원 / 상품 등
  • 무형 엔티티
    • 물리적 형태는 없지만 개념적으로 존재하는 관리 대상, 주로 사건이나 개념을 데이터로 관리할때 나타난다.
    • 주문, 예약, 수강신청, 계좌 등
  • 개념적 모델링 단계 용어들이 데이터베이스 물리적 테이블과는 다음과 같이 연결된다.
    1. 엔티티 -> 테이블
    2. 속성 -> 열, 컬럼
    3. 인스턴스 -> 행
  • 좋은 엔티티 특성은 다음과 같다.
    1. 업무 관련성 (Business Relevance)
      • 해당 업무에 필요하고 관리해야 하는 정보
    2. 식별 가능해야 한다. (Uniquely Identifiable)
      • 엔티티에 속하는 각 데이터는 서로 명확하게 구분될 수 있어야 한다. (인스턴스 ,행)
    3. 두 개 이상의 정보를 가진다. (Has Attributes)
      • 엔티티는 관리할만한 가치가 있는 여러 정보 / 속성들을 가진다.
      • 어떤 대상이 단 하나의 정보만 가지고 있다면 다른 엔티티의 속성일 확률이 높다.
    4. 인스턴스의 집합이다.
      • 엔티티는 개념적인 틀이고, 그 틀에 따라 만들어진 실제 데이터 각각을 인스턴스라 부른다.
      • 엔티티: 회원 / 인스턴스: 회원1, 회원2, ..
    5. 다른 엔티티와 관계를 맺는다.
      • 엔티티는 홀로 존재하지 않고 다른 엔티티와 관계를 맺는다.

# 엔티티 분류

  • 엔티티(데이터 덩어리) 성격과 역할을 제대로 파악하고 구분하는 과정이 엔티티 분류이다.
  • 파악한 데이터 본질을 기반으로 테이블 구조, 키 설계, 관계 설정, 성능 최적화 전략까지 설계의 큰 그림을 그릴 수 있다.
  • 엔티티를 구분하는 주요 기준은 존재 형태, 생성 시점 및 역할, 존재 종속성(독립성)이다.
  • 존재 형태를 기준으로 한 엔티티 분류는 다음과 같다.
    1. 유형 엔티티
      • 물리적 형태를 가지고 있어 눈으로 보거나 만질 수 있는 실체를 표현하는 엔티티
      • 업무로부터 식별이 용이하다. 업무란 현실세계에서 존재하는 비즈니스 활동을 통칭한다.
      • 사원, 학생, 교수, 상품, 강의실, 지점 등
      • 상대적으로 안정적, 지속적으로 활용되는 정보
    2. 개념 엔티티
      • 물리적 형태는 없지만 업무적으로 관리해야 할 중요한 개념이나 아이디어를 표현하는 엔티티
      • 부서, 조직, 계좌, 과목 등
      • 유형 엔티티와 마찬가지로 상대적으로 안정적
    3. 사건 엔티티
      • 업무 프로세스가 진행됨에 따라 발생하는 특정 행위나 사건을 표현하는 엔티티
        • 특정 시점에 발생하며, 비즈니스 활동 결과를 기록하는 역할
      • 업무 수행에 따라 발생하는 행위를 기록
      • 시간의 흐름에 따라 인스턴스가 계속 발생
      • 주문, 계약, 결제 등
    • 엔티티를 어떻게 분류하고 구성하는지 살펴보면 시스템 최종 목적과 정체성을 엿볼 수 있다.
      • 회원 / 상품은 유형 / 개념 엔티티다.
      • 주문은 주문이라는 사건이 발생할때마다 생성되는 사건 엔티티다.
    • 유형, 개념엔티티는 상대적으로 정적이다. 그에 반해 사건 엔티티는 사건이 발생할때마다 인스턴스가 생성되기 때문에 더 동적이다.
    • 위와 같이 데이터의 성격과 증가 추이를 미리 파악하면 테이블 설계 시 여러 전략들을 고려해볼 수 있다.
      • 인덱스 전략: 데이터가 많아진 경우 인덱스를 추가하여 조회 성능을 개선
      • 데이터 파티셔닝 / 아카이빙: 월별 연도별 기준으로 물리적 분리
        • 파티셔닝: 최신 데이터는 더 작은 영역에서 조회하도록 하여 성능 개선
        • 아카이빙: 사용빈도가 극히 낮은 데이터를 별도의 스토리지로 옮겨 운영 테이블을 작게 유지
  • 역할 및 발생 시점에 따른 분류는 다음과 같다.
    • 기본 엔티티(Fundamental / Key Entity)
      • 다른 엔티티에 의해 생성되지 않고 독립적으로 존재할 수 있는 핵심 엔티티
      • 다른 엔티티의 부모 역할을 수행
      • 독립적인 생성 가능
      • 자신만의 고유 주식별자를 가짐
      • 회원, 상품, 사원 등
    • 중심 엔티티 (Main / Center Entity)
      • 기본 엔티티로부터 파생되어 생성 / 업무에서 중심적인 역할을 수행하는 엔티티
      • 기본 엔티티간의 관계에서 발생하는 핵심적인 비즈니스 트랜잭션을 나타내는 경우가 많음
      • 데이터 발생량이 많고, 다른 엔티티와의 관계를 통해 행위 엔티티를 생성하는 허브 역할
      • 주문, 계약, 청구 등
    • 행위 엔티티 (Action / Behavioral Entity)
      • 둘 이상의 부모 엔티티로부터 발생하며, 업무가 흘러가면서 생성되는 상세 정보를 기록하는 엔티티
      • 내용이 자주 변경되거나 지속적으로 증가하는 특징을 가짐.
      • 주문 이력, 결제 내역, 로그 등
  • 다른 엔티티와의 관계 속에서 자신의 존재 여부가 결정되는지 여부에 따라 강한 엔티티와 약한 엔티티로 분류할 수 있다.
    • 강한 엔티티 (Strong Entity)
      • 다른 엔티티 존재 여부와 상관없이 독립적으로 존재할 수 있는 엔티티
      • 자신만의 고유 식별자를 가짐. (PK)
      • 다른 엔티티 존재에 의존하지 않는다.
    • 약한 엔티티 (Weak Entity)
      • 다른 엔티티가 존재하지 않으면 독립적으로 존재할 수 없는 존재 종속적 엔티티
      • 부모 종속적 표현을 강제하기 위해 소유 엔티티 주식별자와 자신의 속성을 결합한 것을 주식별자로 삼는다. (Composite Primary Key)라 부른다.
      • 위와 같은 관계를 논리적 모델링 단계에서 식별 관계라고 한다.
  • 엔티티 분류 후에는 엔티티 속성들 중 식별자를 반드시 선택해야 한다.
  • 식별자는 각 엔티티 인스턴스를 다른 인스턴스와 유일하게 구별해주는 이름표 역할을 한다. DB 테이블에서는 PK로 구현된다.

# 카디널리티

  • 관계 정의 시에는 더 상세한 규칙들을 마련해야 한다.
    • 한 명의 회원이 여러 주문을 할 수 있는지?
    • 주문 없이 회원만 존재 가능한지?
  • 관계의 규칙 정의시 카디널리티(Cardinality)와 참여도(Optionality)가 핵심 요소로 여겨진다.
  • 카디널리티는 한 엔티티 인스턴스가 다른 엔티티 인스턴스와 몇개나 관계를 맺을 수 있는지를 나타내는 수량 제약이다.
    • 카디널리티 관계는 네 종류가 있다.
      1. 일대일(1:1)
        • 주문과 배송
      2. 일대다(1:N)
        • 회원과 주문
      3. 다대일(N:1)
        • 주문과 회원
      4. 다대다(M:N)
        • 주문과 상품 (한 주문에 여러 상품이 포함, 한 상품에 여러 주문이 들어옴)
    • 엔티티 인스턴스 하나가 다른 엔티티 인스턴스와 맺을 수 있는 최대숫자를 기준으로 표현한다.
  • 참여도는 한 엔티티의 인스턴스가 관계에 반드시 참여해야 하는지, 참여하지 않을 수도 있는지를 결정한다.
    • 회원 입장에서 주문은 필수적이지 않다. (Optional)
    • 주문 입장에서 회원은 필수적이다. (Mandatory)

# 관계 표현과 외래 키

  • FK같은 외래 키는 구현 단계에서 사용되는 개념이다.
  • 개념적 모델링 단계에서는 NoSQL에서도 사용 가능할 수 있도록, FK와 같은 특정 구현 방식을 명시하지 않는다.
  • 엔티티간의 관계는 관계선(Relationship)으로 표현하며, 외래키 표현을 위해 엔티티 내에 외래키에 해당하는 컬럼은 포함시키지 않는다.
    • 선으로 이어주게 된다.
  • 이후 논리적 모델링 단계에서 관계선이 외래키로 표현된다.

# ERD(Entity-Relationship Diagram)

  • ERD는 복잡한 데이터 구조를 시각적으로 표현하여 관계자들이 시스템 청사진을 한눈에 파악하고 소통할 수 있게 도와주는 도구이다.
  • ERD 표기법은 피터 첸 표기법과 까마귀발 표기법이 있다.
    1. 피터 첸 표기법
      • 엔티티는 사각형, 관계는 마름모, 속성은 타원으로 표현하는 전통적 방식이다.
      • 실무에서는 사용하지 않는다.
    2. 까마귀발 표기법
      • 실무에서 사용되는 사실상의 표준이다.
      • 엔티티는 사각형 상자로 표현한다.
      • 관계는 두 엔티티를 잇는 직선으로 표현한다.
      • 선의 양 끝에 기호를 붙여 카디널리티와 참여도를 동시에 표현한다.
      • |: 1 One
      • -<-: 발가락 3개의 모양이며, Many를 표현한다.
      • 0: Zero
      • 위 기호들을 조합하여 관계를 표현한다.
      • Zero와 섞어서 선택적 데이터인지 (0개 데이터도 가능한지) 필수적 데이터인지 (1개 이상 반드시 데이터를 가져야 함) 표현 가능하다.
      • 까마귀발 표기법에서 가장 중요한 원칙은 한 엔티티 끝쪽에 있는 기호는 반대편 엔티티 규칙을 설명한다는 것이다.

# 다대다 관계의 해결

  • 다대다 관계는 개념적으로 타당하지만, 해결 불가능한 문제가 존재한다.
  • 우선 물리적으로 구현 불가능하다는 점이 있다.
    1. 개념적으로 그리는 것은 쉽지만 실제 데이터베이스 테이블 구조로 옮길 수 없다.
      • 주문 아이디를 PK로, 상품 아이디를 FK로 설정한다.
      • 주문 아이디 101에 대해 상품 아이디 1001, 1002가 묶인다고 가정하자.
      • 이때 상품 아이디 1002 값을 갖는 인스턴스가 다른 주문아이디도 개념상으로는 가질 수 있다.
      • 이 경우 상품 아이디 1002에 대해서는 어떤 주문 아이디를 가져야 하는 지에 대해 구현이 불가능하다.
    2. 한 주문 당 상품 최대 갯수를 정하고 남는 공간은 NULL로 표현한다.
      • 최대 갯수가 늘어나거나 줄어드는 경우 큰 문제가 발생
      • 불필요한 NULL 공간 차지
      • 조회성능 저하
    3. 한 컬럼 내에 구분자로 값 욱여넣기
      • 조회의 어려움
      • 데이터 수정 및 삭제가 어려움
      • 데이터 원자성 위반
  • 둘째로, 관계에 속한 데이터를 저장할 장소가 없다.
    • 주문 이후 처리 과정에서 중간에 상품 가격이 변경되었다고 가정해보자.
    • 상품 테이블에는 변경된 가격 값이 반영된다.
    • 주문 당시 변경 전 가격 값을 원본 테이블에서 참조하게 되면 주문 이후 시점에도 변경된 값을 참조해버리게 된다.
      • 변경 전 가격 값을 테이블에 저장하는 것은 주문 테이블의 목적성에 부합하지 않는다.
  • 이러한 문제를 해결하는 방법은 관계를 엔티티로 승격하는 것이다.
    • 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 기반으로 데이터를 욱여넣는 것은 원자성을 위반하게 된다.
  • 일대다 조인과 다대일 조인 시 데이터 뻥튀기 현상을 이해해야 한다.
    1. 다대일 조인
      • FK -> PK로 조인
      • 뻥튀기가 발생하지 않는다.
    2. 일대다 조인
      • PK -> FK로 조인
      • 뻥튀기가 발생한다.

# 일대일 관계

  • 일대일 관계는 하나의 테이블로 합치는 게 효율적인 경우가 많지만, 그럼에도 이러한 관계를 맺는 데에는 이유가 있다.
    • 두 엔티티가 항상 함께 조회되는 케이스 (User - User Profile)
    • N쪽 테이블이 필수 참여일때 그냥 합쳐도 됨
  • 일대일 관계가 의미있는 경우
    1. 성능 최적화
      • 모든 부가정보가 하나의 테이블에 포함되어 있다면 불필요한 데이터 조회에 의한 성능 저하가 발생한다.
    2. 보안 강화
      • 민감정보는 테이블로 분리하고, 테이블 접근권한을 별도로 부여한다.
    3. 선택적 참여 여부 표현
      • 1대1에서 한쪽이 선택적 참여인 경우 테이블 분리가 필요하다.
    4. 비즈니스 모델
      • 엔티티 사이의 성격이 아주 상이할때
      • 관리 주체나 라이프사이클이 다른 데이터를 관리해야 할때 분리한다.
  • 일대일 관계 구현시에는 반드시 FK쪽에 UNIQUE 제약조건을 걸어두어야 한다.
  • 일대일 관계에서 FK 위치는 어느 곳이던 문법적으로는 문제가 되지 않는다.
    • 일반적인 경우, 보조 테이블에 FK를 둔다.
      • 비즈니스 요구사항에 의해 보조 테이블이 N 테이블로 변경될 수도 있다.
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. 제1 정규형을 만족해야 한다.
    2. 테이블의 모든 컬럼이 기본 키에 대해 완전 함수 종속이어야 한다.
      • 부분 함수 종속이 없어야 한다.
  • 부분 함수 종속에 해당하는 컬럼들을 모아 별도의 테이블로 분리해야 한다.

# 제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에 초점을 맞추어 모델링을 진행한다.
  • 물리적 테이블, 컬럼, 데이터 타입 정의, 인덱스 생성, 파티셔닝 설계, 스토리지 엔진, 프로시저 등 실질적 구현 진행
  • 물리적 모델링의 프로세스는 다음과 같다.
    1. 테이블과 컬럼 변환
      • 논리적 모델의 한글명을 영문으로 변경한다.
    2. 데이터 타입 정의
      • 각 컬럼에 적합한 데이터 타입을 선택한다.
      • 저장공간과 성능에 직접적인 영향을 준다.
    3. 제약 조건 설정
      • PK / FK / NOT NULL 등 데이터 무결성 보장을 위한 제약조건을 설정한다.
    4. 인덱스 설계
      • 데이터 조회 성능 극대화를 위해 인덱스를 설계한다.
    5. 역정규화 및 성능 튜닝
      • 필요에 따라 정규화 원칙을 위배하여 테이블을 통합하거나 중복 데이터를 추가하는 역정규화를 실시한다.
    6. 파티셔닝, 샤딩 등 적용
      • 대용량 테이블의 경우 데이터를 분할하여 저장하는 기법을 사용
    7. 뷰, 프로시저, 함수, 트리거 등 사용

영문명 작성 컨벤션

  • 테이블의 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의 빈도가 늘어나고, 이 경우 시스템 성능을 저하시키게 된다.
  • 역정규화는 데이터 일관성 및 정합성을 일부 희생하고, 조회 속도를 얻는 트레이드오프 관계에 있다.
  • 역정규화에는 여러 기법들이 존재한다.
    1. 중복 컬럼 추가
      • 조인 연산 후 가져올 컬럼 데이터를 그대로 복사하여 가져온다.
      • 데이터 불일치 가능성이 커지기 때문에 트리거와 같은 데이터 동기화 로직들을 추가 구현해야 한다.
    2. 파생 컬럼 추가
      • 조회 시점에 SUM, COUNT와 같은 연산이 자주 이루어지는 경우 해당 계산 결과를 미리 컬럼에 저장해둔다.
      • 쓰기 작업에 부하를 주며, 값 업데이트 시 갱신하는 로직을 구현해야 한다.
    3. 테이블 통합 및 분할
      • 항상 함께 조회되는 테이블을 합쳐 JOIN을 제거한다.
      • 한 테이블의 컬럼이 대량이고, 일부 컬럼만 사용하는 경우 수직분할을 수행한다. 컬럼 4개가 있을때 자주 사용되는 컬럼 전용으로 2개, 2개로 분할하는 방식을 예로 들 수 있다.
  • 역정규화로 인한 데이터 불일치 문제는 다음 방식들로 개선 가능하다.
    1. 애플리케이션 로직: 개발자가 책임
    2. 데이터베이스 트리거: 특정 테이블에 INSERT, DELETE, UPDATE 등의 이벤트 발생 시 정의해둔 쿼리 수행
    3. 배치 작업: 데이터 불일치 문제를 트래픽이 없는 시간대에 몰아서 처리
  • 반드시 논리적 모델링 과정에서 정규화를 모두 마친 뒤, 물리적 모델 구현 후 운영 과정에서 발생하는 이슈에 따라 필요한 부분에 한해 역정규화를 적용해야 한다.