- 식별자
: 데이터를 식별할 수 있는 속성- 대체여부에 따라 분류가능
- 본질식별자 : 업무에 의해 만들어진 식별자
- 인조식별자 : 업무적으로 만들어지지는 않았지만 본질식별자가 복잡한 구성을 갖고있으므로 인위적으로 만든 식별자
위 모델의 주문상품 모델의 식별자가 본질식별자이다. 주문상품 모델은 주문 시 구매한 상품 정보를 관리한다.
주문번호 | 상품번호 | 주문수량 |
---|---|---|
110001 | 1234 | 1 |
110001 | 1566 | 5 |
110001 | 234 | 2 |
위 표는 하나의 주문에 3개의 상품을 구매한 것을 데이터로 표현한 것이다.
이러한 데이터로 개발을 진행하여 주문상품 모델에 값을 Insert하는 경우를 생각해보자
해당 주문에 구매한 상품에 대한 정보를 Insert하면 된다.
INSERT INTO 주문상품 VALUES(110001, 1234, 1);
INSERT INTO 주문상품 VALUES(110001, 1566, 5);
INSERT INTO 주문상품 VALUES(110001, 234, 2);
하지만 종종 다음과 같은 모델이 목격된다.
위 모델은 주문상품번호라는 새로운 식별자를 생성하였다. 이 식별자를 외부식별자라고 한다.
이와 같은 모델의 Insert문은 다음과 같다.
INSERT INTO 주문상품 VALUES(주문상품번호SEQ.NEXTVAL, 110001, 1234, 1);
INSERT INTO 주문상품 VALUES(주문상품번호SEQ.NEXTVAL, 110001, 1566, 5);
INSERT INTO 주문상품 VALUES(주문상품번호SEQ.NEXTVAL, 110001, 234, 2);
위 SQL문은 '주문상품번호SEQ'라는 시퀀스객체를 생성하고 NEXTVAL기능을 이용하여 자동으로 값을 채번하여 Insert하는 방식이다.
사진2-21모델에 비해 전혀 좋은점이 없다. 오히려 불필요한 시퀀스를 생성할 뿐이다.
그렇다면 왜 이런 모델을 생성하였을까? 실제 해당 엔터티의 본질식별자에 대한 고민을 하지 않았기 때문이다.
그럼 조금 다른 모델의 경우를 살펴보도록 하자.
위 모델의 주문상세 모델은 상품번호를 식별자로 구성하지 않고 하나의 주문에 발생하는 상품의
Count를 주문순번이라는 속성으로 식별자를 구성하였다.
-- 본실식별자 주문상세--
주문번호 | 주문순번 | 상품번호 | 상품명 | 배송지 |
---|---|---|---|---|
110001 | 1 | 1234 | 제주감귤 1box | 우리집 |
110001 | 2 | 1234 | 제주감귤 1box | 부모님집 |
110001 | 3 | 1234 | 제주감귤 1box | 친구집 |
표 데이터를 보면 동일상품을 하나의 주문에서 처리하고 있다. 즉 쇼핑몰에서 동일한 상품 몇 개를각기 다른 배송지에 보내고 싶은 요건을 나타낸 것이다.
INSERT INTO 주문상세 VALUES(110001, 1, 1234, '제주감귤 1box', '우리집');
INSERT INTO 주문상세 VALUES(110001, 2, 1234, '제주감귤 1box', '부모님집');
INSERT INTO 주문상세 VALUES(110001, 3, 1234, '제주감귤 1box', '친구집');
위 모델의 Insert문이다. 이전 모델과 다른 점은 주문순번 값을 위해 하나의 주문에 구매하는 상품의 Count를 계산하여 입력해야한다. 이와 같은 작업은 어려운 일은 아니지만 번거로운 추가된 것이다.
그리하여 다음 아래와 같은 모델을 종종 발견할 수 있다.
위 모델은 식별자를 주문상세번호로 정의하였다. 이전 모델과 차이점이라고 하면 식별자를 하나의 속성으로 구성한 외부식별자로 생성한 것이다. 주문속성 속성이 사라졌지만 대신 주문상세번호가 생성되었다. 큰 차이는 없어보이지만 실제 개발시 편의성이 향상되는 방식이다.
아래의 데이터로 어떤 부분에서 개발의 편의성이 향상되었는지 살펴보도록 하자.
-- 인조식별자 주문상세 --
주문상세번호 | 주문번호 | 상품번호 | 상품명 | 배송지 |
---|---|---|---|---|
1 | 110001 | 1234 | 제주감귤 1box | 우리집 |
2 | 110001 | 1234 | 제주감귤 1box | 부모님집 |
3 | 110001 | 1234 | 제주감귤 1box | 친구집 |
본질식별자주문상세 표와 비교해보면 주문순번이 주문상세번호로 바뀌었다. 다른 점이 없어 보이지만 실제 해당값을 구하는 방식에서 차이점을 알 수 있다.
- 주문순번
: 하나의 주문번호에 대해 구매가 일어나는 상품의 Count를 구하는 것으로 시퀀스 객체를 활용할 수 없어 따로 작업을 해줘야 한다.- 주문상세번호
: 단일식별자로 구성된 키값이기 때문에 시퀀스 객체로 해결이 가능
아래의 SQL문을 보면 더 명확하게 알 수 있다.
INSERT INTO 주문상세 VALUES(주문상세번호SEQ.NEXTVAL, 110001, 1234, '제주감귤1box', '우리집');
INSERT INTO 주문상세 VALUES(주문상세번호SEQ.NEXTVAL, 110001, 1234, '제주감귤1box', '부모님집');
INSERT INTO 주문상세 VALUES(주문상세번호SEQ.NEXTVAL, 110001, 1234, '제주감귤1box', '친구집');
하지만 위 방식처럼 외부식별자를 사용하는 방식에는 크게 두가지 문제점이 있다.
- 중복 데이터로 인한 품질
- 불필요한 인덱스 생성
외부식별자를 사용하면 중복 데이터를 막을 수 없다. 기본키의 제약을 활용한다면 중복데이터를 원천 차단할 수 있지만, 기본키를 인위적으로 생성한 속성으로 정의하였기 때문이다.
INSERT INTO 주문상세 VALUES(주문상세번호SEQ.NEXTVAL, 110001, 1234, '제주감귤1box', '우리집');
INSERT INTO 주문상세 VALUES(주문상세번호SEQ.NEXTVAL, 110001, 1234, '제주감귤1box', '우리집');
INSERT INTO 주문상세 VALUES(주문상세번호SEQ.NEXTVAL, 110001, 1234, '제주감귤1box', '부모님집');
INSERT INTO 주문상세 VALUES(주문상세번호SEQ.NEXTVAL, 110001, 1234, '제주감귤1box', '친구집');
위 SQL의 두번째 Insert문이 로직 오류로 인해 중복으로 발생되었다고 하자.
기본키를 인위적으로 인조식별자로 구성하였으므로 기본키 제약은 주문상세번호에 대해 적용되어 있기 때문에 중복된 데이터를 막을 수 없다.
그로인해 실제 데이터는 다음과 같이 저장되었을 것이다.
-- 인조식별자 주문상세 --
주문상세번호 | 주문번호 | 상품번호 | 상품명 | 배송지 |
---|---|---|---|---|
1 | 110001 | 1234 | 제주감귤 1box | 우리집 |
2 | 110001 | 1234 | 제주감귤 1box | 우리집 |
3 | 110001 | 1234 | 제주감귤 1box | 부모님집 |
4 | 110001 | 1234 | 제주감귤 1box | 친구집 |
위와 같이 중복으로 발생된 데이터임에도 저장이 된것을 볼 수 있다.
왜냐하면 주문상세번호에 기본키 제약이 적용되어 있고, 주문상세번호는 시퀀스를 사용하였기에 제약에 위배한 사항이 없기 때문이다.
정리해보자면 최대한 본질식별자를 지향해야 한다. 만일 외부식별자를 사용했다면 DBMS에서는 막아줄 수 없기때문에 어플리케이션에서 막아주어야 한다.
본질식별자와 인조식별자를 사용했을 때 인덱스 구성에 대해 어떤 차이가 있는지 알아보도록 하자.
주문상품 모델데이터에 엑세스한다고 가정해보자. 가장 기본적인 엑세스 패턴은 다음 SQL과 같을 것이다.
SELECT *FROM 주문상품 WHERE 주문번호 = :B1;
이러한 SQL에 대해 본질식별자로 구성하면 PK인덱스를 활용할 수 있겠지만, 인조식별자로 구성한다면 위 사진의 IX1과 같은 인덱스를 추가로 생성해주어야 한다. 즉 인조식별자를 사용한다면 불필요한 인덱스를 추가로 생성해야 하는 점을 기억해야 한다. 또한 추가로 생성한 인덱스는 용량과 DML 성능에 영향을 줄 수 있음을 염두에 둬야한다.