March 18, 2021
도메인은 관계형 모델에서 데이터형에 불과하다. 도메인의 집합으로 정의된다.
→ 도메인은 속성이 취할 수 있는 값의 집합이다.
도메인의 요소에 어떤 구조를 가진 데이터를 할당할 것인지는 관계형 모델상의 제한은 없다.
집합의 요소로 정의할 필요가 있으므로 명확한 값을 가진 것에 한한다. NULL이나 포인터는 집합의 요소로써 사용할 수 없으므로 도메인의 요소로 쓸 수 없다.
도메인이 유한 집합이긴 하지만 도메인에 포함될 가능성이 있는 값을 모두 열거할 필요는 없다. 요소가 될 수 있는 값은 무수히 존재한다.
각 속성에 어떤 값이 적합한지 생각하는 것은 설계자의 업무다.
→ 도메인을 SQL 상에서 표현하는 것.
관계형 모델에 적합하도록 SQL을 사용하는 것은 컬럼의 값이 항상 무언가의 집합(도메인) 요소가 되도록 컬럼을 설계하지 않으면 안된다는 의미다.
속성이 어떤 값을 얻을 수 있는지 결정하는 것은 응용프로그램이다.
DB 쪽의 설계를 수행하기에 앞서 응용프로그램의 설계, 즉 응용프로그램이 무엇을 수행하고 어떤 데이터가 입출력되며 이를 위해서 어떤 데이터를 영속화해야 하는지 찾아야 한다.
적절한 DB 설계는 응용프로그램에 관한 이해 없이는 할 수 없다.
뛰어난 응용프로그램을 설계하려면 적어도 어떤 것이든지 응용프로그램의 설계 방법을 익혀야 한다.
바로 도메인 주도 설계(Domain-Driven Design, DDD) 다.
도메인 주도 설계는 응용프로그램에 어떤 데이터나 로직이 필요한가를 알아내는 것은 DB에 어떤 릴레이션이나 속성이 필요하냐는 주제에 직결된다.
도메인 주도 설계는 여러 번 리팩터링 하면서 이와 같은 응용프로개름의 본질을 파악하게 된다.
도메인 주도 설계뿐만 아니라 응용프로그램의 개발에는 여러 번의 리팩터링을 실시한다.
그러나 코드는 여러 번 리팩터링해도 DB 설계는 언제까지나 같은 것을 계속 사용하는 경우가 있다.
→ 즉, 응용프로그램뿐만 아니라 DB도 리팩터링 해야 한다.
도메인을 설계할 때는 자릿수와 같은 표시상의 문제나 사용자의 편리성 등 본질적인 데이터를 확실하게 구분하자.
DB가 다루는 것은 본질적인 데이터뿐이다.
ex)
인간에게 편리성 - 학생 ID를 8자리로 만들어 보기 편하게 함
그러나 본질적인 데이터는 그렇지 않다. 자리수는 표현상의 문제임.
속성의 이름에 명명 규칙을 통일한다
, 주어를 포함한다
와 같은 노하우가 있지만 더 좋은 속성의 이름을 정하기엔 부족하다.
가장 중요한 점은 속성이 나타내는 데이터의 보닐을 표현하는 이름을 사용하는 것이다.
→ 이름은 본문을 나타내는
속성명이어야 한다.
각 속성의 의미가 정확하지 않으면 그 속성 자체에 붙이는 것은 물론이고 속성의 집합인 제목(릴레이션)이 나타내는 의미를 제대로 이해하는 것은 불가능할 것이다.
대부분의 릴레이션(테이블)은 응용프로그램의 어떤 클래스와 1:1로 대응되어 있다.
응용프로그램에서 사용되는 클래스명이나 변수명과 같은 이름을 사용하도록 하고, 만약 응용프로그램에서 클래스명이나 변수명을 리팩터링했다면 DB쪽도 동기화하도록 하자.
DB에 저장된 ID는 현실 세계의 물체나 개념을 나타낸다.
물체나 개념을 집합으로 표현하는 것이 관계형 모델의 기본적인 개념이다.
→ 한 개의 속성이 한 개의 물건이나 개념을 나타낸다.
예를 들어 ‘가위’라는 개념에 ID로 사용해보자.
세상에는 많은 종류의 가위가 존재하기 때문에 같은 종류의 개체도 많이 존재할 것이다. → 현실의 물건을 나타내는 전단사 함수는 될 수 없으며(1:1) 현실 세계의 ID로 사용하는 것은 부적절하다.
다른 설계에서는 가위의 제품번호마다 꼬리표를 붙여서 사용할 수 있을 것이다.
ID를 설계하는 경우는 그것이 개체에 대한 것인지 집단에 대한 것인지, 집단이라면 어떤 크기로 집합을 식별화할지 등을 고려해야 한다.
또다른 관점에서는 매우 제한된 세계 속의 이야기 일수도 있다. 예를 들어 ‘어떤 책상 위에 있는 물건’, ‘추리게임에 나오는 아이템’ 이라는 설정한 경우.
이처럼 매우 제한된 세계에서 아주 작은 물건밖에 존재하지 않을 때는 각 물건을 고유하게 식별할 수 있다.
→ 의제영역이 한정된 경우라면 세상에서 유일성을 갖지않는 꼬리표라도 ID로 기능할 수 있다.
둘 다 RDB에서는 필요하며 상황에 따라서 구분해야 한다.
일반적으로 자연키가 바람직하지만 중복될 가능성이 있다. 아주 대표적 예시로 사람 이름이다. 이름은 사람을 특정하는 데 사용하는 꼬리표이며 DB 밖의 세계에도 존재한다. → 이를 속성으로 사용하면 자연키가 된다.
그러나 규모가 커져 사람 수가 늘어나면 같은 이름을 가진 사람이 나올 확률이 높아질 것이다. 이름이 겹치면 이름은 더는 사람을 특정하기 위핸 ID로써 사용하는 기능을 잃게 될 것이다.
ID로써 사용할 수 있다는 것, 즉 현실 세계의 물건이나 개념과 1:1로 대응하는 집합이라면 자연키를 사용해도 문제가 없다.
DB 설계상 자연키를 사용하는 것은 아무런 문제가 되지 않는다.
→ 문제는 사용값이 ID로써 기능할 수 있는지 아닌지다.
이미 누군가가 운영하고 무언가로 식별할 수 있고, 오랜 시간을 거친 값에 변경이 없고 신뢰할 수 있다고 생각되는 것은 자연키로 사용해도 문제 없을 것이다. (예시 - 미국의 사회보장보험번호)
이러한 ID를 자연키로 채용한다면 해당 ID의 라이프 사이클을 확실하게 알아야 한다.
ID도 실제로는 운영이 잘못돼 중복이 발생할 때도 있다. 그 예시가 도서의 ISBN이다. 실제로 일부 도서는 ISBN이 중복되고 있다. 중복이 발생하면 ID로써의 기능을 잃어버리므로 자연키로 사용하는 것은 위험하다.
ID가 무엇을 특정하는 것이냐는 관점도 중요하다. 자주 범하는 실수 중 하나로 이메일 주소를 사람을 식별하는 ID로 사용하는 경우가 있다. (물론 이메일 주소는 유일해서 중복되지 않음)
그런데 이메일 주소는 어디까지나 이메일 주소를 식별하는 것이며 사람을 특정하는 것은 아니다.
→ 사람과 1:1(전단사 함수)로 일치하지 않는다.
그래서 어떤 시스템에서 사용자 ID를 표현하는데 이메일 주소를 자연키로 사용하는 것은 위험하다.
그 값이 무엇을 특정하는 것인지 생각해야 한다.
식별하고 싶은 대상의 물건이나 개념을 나타내는 ID가 아직 세상에 존재하지 않을 때가 많이 있다.
이 때 사용하는 것이 대체키이며 새로운 시스템이 ID를 할당하는 데 아무런 문제가 없다.
문제는 이미 자연키가 있는데 대체크를 새로 생성하는 경우다.
새롭게 대체키를 작성해도 본래의 자연키에 유니크키 제약이 필요할 것이다. SQL은 해당 테이블을 갱신하면 유니크 키를 갱신하기 위한 오버헤드가 발생한다. 인덱스 갱신을 위한 오버헤드가 높아지므로 불필요한 인덱스는 가능한 한 작성해선 안된다.
만약 갱신의 오버헤드가 싫어서 본래의 자연키에 유니크키 제약을 제거하면 자연키에 중복이 발생할 수 있고 DB에 변칙이 생길 것이다.
→ 이미 적절한 자연키가 존재할 때는 본질적으로 대체키는 불필요하다.
또 다른 실수는 복합 기본키가 싫어서 대체키를 추가하는 경우다.
관계형 모델에서는 후보키가 여러 개의 속성으로 구성되는 게 지극히 자연스러운 것이다.
불필요한 대체키를 추가하면 본래의 키 → 대체키, 또는 대체키 → 본래의 키라는 함수 종속성이 생기게 된다.
만약 이와 같은 릴레이션은 무손실 분해를 해야 할까? 불필요한 대체키는 DB 설계를 불필요하게 복잡하게 만든다.
사실 자연키, 대체키는 관계형 모델의 개념에는 없다. 관계형 모델에 있는 것은 후보키, 슈퍼키뿐이고 키라는 기능은 단순히 튜플에 포함된 속성의 값을 유일하게 결정할 수 있다는 것(키의 값을 알면 다른 속성의 값도 알수 있음)에 지나지 않는다. 또한, 슈퍼키 중에 기약인 키가 후보키라는 것뿐이다.
키가 자연키이든 대체키이든 마찬가지로 관계형 모델을 기반으로 한 설계 논리를 적용할 수 있다. 자연키와 대체키는 도메인 설계에 관한 주제이며 관계형 모델 그 자체의 주제는 아니니 혼동하지 말 것.
ID에 의미를 부여하는 설계를 자주 볼 수 있다. 예시로 제품 코드에 제품의 카테고리나 색상과 같은 특지을 담아서 여러 단어를 하이픈으로 이어놓은 것들이 있다.
이와 같은 ID는 현실의 물건과 1:1로 대입되면서 ID 기능이 있다. 그런데 DB에서 하나의 속성으로 다루기에는 문제가 있다.
문제가 되는 것은 이와 같은 ID 일부분에 의존한 처리가 생길 때다. 예시로 청소기에 ‘CLN-CYC-0123-BL’라는 제품코드를 할당했다고 쳤을 때, 제품 목록의 테이블에서 해당 청소기를 추출하려면 다음처럼 쿼리를 짜야한다.
SELECT * FROM product_list
WHERE product_id LIKE 'CNL%' AND
product_id LIKE '%BL';
이러한 쿼리가 되는 이유는 ID가 1NF의 요건을 만족하지 않기 때문이다. 이 ID를 여러 부분으로 나눌 수 있으므로 각 부분에 개별 속성을 정의해야 한다.
DB에서는 부분별로 나눈 값을 저장하고 제품 코드로 표시할 때는 값을 조합해 코드로 만드는 것이 좋다. 응용프로그램에서는 DB 상의 표현과 실제 표현을 서로 변환하는 로직을 갖고 있어야 한다.
그런데 제품 코드처럼 여러 개의 부분으로 키가 성립할 때는 다시 대체키를 도입하는 것이 나쁜 선택이 아니다. 제품코드는 제품의 종류를 특정하는 ID임에도 제품 코드를 부분별로 다른 컬럼에 저장할 경우 제품과 1:1로 일치하는 ID가 되는 속성이 그 릴레이션에는 존재하지 않기 때문이다.
예를 들어 ‘CLN-CYC-0123-BL’이라는 제품 코드는 번호를 가지고 있다. 제품 카테고리 내에서는 고유한 값일지는 모르지만, 전체 제품 중에서 개별 제품을 식별할 때는 도움이 되지 않는다.
→ 원래의 ID 설계가 나쁘므로 실제 ID가 되어야 할 속성이 없는 것이다.
ID 일부에 의미를 부여하는 설계는 피해야 한다.
적절한 ID를 선택하지 않으면 그 문제의 영향 범위는 그 ID를 키로 하는 릴레이션만으로 그치지 않는다.
→ ID를 잘못 선택하면 온갖 릴레이션에 영향을 미치게 될 것이다.
중요한 것은 적절한 SQL의 데이터형을 선택하는 것이다. 도메인 자체와 SQL의 각종 데이터형은 1:1로 대응하지 않지만 적어도 도메인이 가지는 값을 모두 포함할 수 있는 데이터형을 선택해야 한다.
도메인에서 SQL의 데이터형으로 대상이 전부 투영되지 않으면 안된다는 의미다. 수치라면 충분한 자릿수가 있는지 문자열이라면 충분한 길이를 가지는지 고려해야 한다.
자주 실수하는 중 하나가 수치형 ID를 문자형 컬럼으로 테이블을 정의하는 경우다. 이런 컬럼 설계는 문제점이 많다. 예를 들어 불필요하게 테이블 사이즈가 커지고, 수치외에 문자도 저장할 수 있어야 하고, 숫자로 연산할 수 없는 등의 이유를 들 수 있다.
→ 도메인의 특징을 가장 잘 나타내느 데이터형을 사용하는 것이 중요하다.
컬럼이 도메인의 특징을 잘표현하려면 도메인과 그 컬럼의 데이터형이 1:1 관계로 되어 있는 것이 바람직하다. 도메인과 컬럼의 대응을 단사(X → Y함수)에 가깝게 하는 방법으로 CHECK 제약 조건을 들 수 있다.
도메인 술어의 의미를 수식 등으로 표현할 수 있다면 CHECK 제약을 사용해 그 컬럼에 저장할 수 있는 데이터를 제한한다. 이를 통해 도메인에 포함되지 않는 값을 어느 정도 제거 할 수 있다.
도메인과 컬럼이 전단사(중복 없이 1:1) 되는 것이 이상적이지만 모든 컬럼에 이런 제약을 거는 것은 현실적이지 않다. 제약을 거는 데 손도 많이가고, 제약이 늘어나면 오버헤드도 커지게 된다.
도메인에 포함된 어떤 값의 종류(cardinality)가 적고 CHECK 제약으로 표현하기 어려울 때, 즉 도메인이 열거형이면 다른 테이블에 미리 도메인에 포함된 모든 값을 저장해 두는 방법이 있다.
일반적으로 이런 테이블을 마스터 테이블이라고 한다. 도메인은 집합이므로 술어를 정의하지 않고 마스터 테이블 내에 실 데이터를 열거하는 형태로 정의해도 된다.
DB 중에 데이터형으로 ENUM형(열거형)을 지원하는 것도 있다. 데이터 사이즈가 작고 오버헤드도 적어서 적합한 곳이 있다면 마스터 테이블 대신 써보는 것도 좋다.