ERD를 설계하다 알게 된 몇 가지 사실

최창효·2023년 8월 5일
1
post-thumbnail

간단한 쇼핑몰 ERD 설계 과제를 진행하다 알게된 내용에 대해 정리한 글입니다.

정답보다는 제 생각에 가까운 글이므로 틀린 내용이 존재할 수 있습니다. 틀린 내용이 있다면 알려주시면 감사하겠습니다.

식별 관계를 사용하기 좋은 경우

식별 관계란 부모 테이블의 PK가 외래키로 사용되면서 자식 테이블의 PK에 포함되는 경우를 말합니다.
식별 관계에서 자식 테이블의 값을 생성하려면 반드시 부모의 값이 먼저 존재하고 있어야 합니다.
또한 자식 테이블의 PK에 다른 값을 포함하지 않고 자식 테이블의 PK를 오로지 부모 테이블을 PK만으로 구성하면 부모가 동일한 값이 중복되어 생성될 수 없습니다. 부모가 동일하다는 건 곧 PK가 중복된다는 걸 의미하기 때문입니다.


쇼핑몰에서 회원은 마음에 드는 제품을 찜해둘 수 있습니다. 이 경우 'A회원이 a상품을 찜했다'는 내용은 여러 개 있을 이유가 없습니다. 이럴때 찜하기 테이블이 회원의 PK와 제품의 PK를 식별 관계로 가진다면 한명의 유저는 하나의 상품에 대해 한번만 찜할 수 있게 됩니다.

이 방법은 SELECT * FROM like WHERE user_id = A AND product_id = a를 통해 찜하기 여부를 확인할 필요가 없습니다. 동일한 값으로 삽입을 시도하면 때문입니다.

'찜했던 상품을 한번 더 클릭하면 찜하기가 취소되는 기능'을 설계할 때는 적합하지 않을 수 있습니다.

예제 코드

create table users(
	user_id INT,
    primary key(user_id)
);

create table product(
	product_id INT,
    primary key(product_id)
);

create table likes(
	user_id INT,
	product_id INT,
    FOREIGN KEY(user_id) REFERENCES users(user_id),
    FOREIGN KEY(product_id) REFERENCES product(product_id),
    PRIMARY KEY(user_id, product_id)
);

insert into users values (1);
insert into product values (1);
insert into likes value(1,1);
insert into likes value(1,1); // Error. Duplicate entry '1-1' for key 'likes.PRIMARY'

select * from likes; // 데이터는 하나만 들어가 있음

product_order가 product의 price에 의존할 때 문제점

해당 예시는 '여러 상품을 한번에 주문할 수 있는 기능'에 대한 테이블을 구현하는 단계입니다.
제품과 주문이 존재하고 둘은 M:N의 관계를 가지기 때문에 이를 product_order라는 중간테이블로 풀어서 설계할 수 있습니다.

이러한 설계는 '1번 주문에서 A상품, B상품, C상품을 구매'했다면 다음과 같이 데이터가 저장됩니다.

order 테이블

idstatus
1주문 완료

product_order 테이블

idproduct_idorder_id
1A1
2B1
3C1

현재 설계에서 '1번 주문에서 구매한 C상품의 가격'을 알려면 관계를 타고 직접 product 테이블로 이동해 값을 찾아와야 합니다. 이처럼 주문 내역에서 구매 가격을 product에서 직접 찾는 방법은 좋지 못합니다.

2012년 C상품을 구매할 당시 C상품은 1000원이었는데 2023년 C상품의 가격이 5000원으로 변해있으면 어떻게 될까요? 위 설계에서는 product에서 직접 값을 찾기 때문에 5000원을 가지게 됩니다. 하지만 실제로 우리는 C상품을 5000원이 아닌 1000원에 구매했습니다.

이러한 문제를 방지하기 위해 product_table은 구매 이력이 생성되는 시점에 해당 물품의 가격을 직접 가지고 있는 게 좋습니다.

product_order 테이블

idproduct_idorder_idprice
1A1100
2B12000
3C11000

이제 product 테이블의 가격이 변경되더라도 우리의 구매 내역에는 항상 구매한 시점의 가격을 확인할 수 있습니다.

자기참조를 사용하기 좋은 경우

물건에 '대분류 - 중분류 - 소분류'의 카테고리가 존재한다고 가정해 봅시다. 이때 다음과 같이 카테고리를 설계할 수도 있습니다.

이 경우 중분류가 존재하지 않는 상품을 추가하기 어렵다는 문제가 있습니다. 더 큰 문제는 '대 - 중 - 소'이외에 더 세부적인 분류를 도입했을 때 점점 테이블이 복잡해진다는 문제가 있습니다.

이러한 문제를 해결하기 위해 아래와 같이 모든 종류의 카테고리를 하나의 테이블에 모으고 자신의 상위 카테고리를 fk로 보관하는 자기참조를 활용할 수 있습니다.

자기참조를 활용해 부모가 없는 카테고리도 활용할 수 있으며, 카테고리의 계층이 확장돼도 문제가 없습니다.

자바의 Enum으로 표현되는 Status와 같은 데이터에 대해

주문 데이터는 보통 주문 신청, 주문 취소, 주문 완료, 대기, ...와 같은 상태를 가집니다.

저는 이러한 상태 데이터를 Java의 Enum클래스로 Application Level에서 관리했었습니다.

하지만 이번에 이러한 상태 데이터를 데이터베이스에서 테이블로 관리하기도 한다는 얘기를 들었습니다.

Status를 DB에 보관하기

간단하게는 이전에 활용했던 상태 데이터들이 모두 DB에 기록된다는 점과 상태값이 무수히 많아졌을 때 코드가 복잡해지는 걸 방지할 수 있다고 생각했습니다.

또한 시스템 운영 측면에서도 유리한 점이 있다는 생각이 들었습니다. 만약 주문 상태에 보류라는 새로운 값이 추가됐을 때 Application Level에서 상태를 관리하면 Backend서비스를 잠시 중단한 뒤 코드를 수정하고 재배포해야 하지만, 데이터베이스로 관리하면 DB에 직접 값을 넣거나, 상태를 추가하는 API가 존재한다면 이를 통해 Backend서비스의 중단 없이 상태를 추가할 수 있어 좋은 방법이라 생각했습니다.

모든 Status를 하나의 DB에 보관하기

주문뿐만 아니라 Domain의 많은 곳에서 이와 유사한 상태 데이터를 가질 수 있습니다. (ex - 재고 데이터는 출고됨, 입고됨, 반품됨, ... 의 상태를 가질 수 있습니다.)

그렇다면 이러한 Status테이블은 상태가 필요한 Domain마다 생성되어야 할 수 있습니다. 하지만 이러한 경우 관리해야 할 테이블이 너무 많아지는 문제가 존재할 수 있습니다.

이러한 문제를 해결하기 위해 아래처럼 모든 테이블의 Status가 하나의 테이블에 변수로 들어가게 설계하는 방법도 있다고 합니다.

상태 테이블

iddomain_namestatus
1주문주문 신청
2재고출고됨
3주문주문 취소
4주문주문 완료
5재고입고됨

직접 위와 같은 설계로 시스템을 운영해보지 않아 정확한 판단은 어려우나 얘기만 들었을 때는 개인적으로 부정적인 입장이었습니다. 그렇게 생각한 이유는 다음과 같습니다.

  • 수많은 테이블들이 해당 테이블과 관계를 맺게 되며 SPOF(단일 장애점)이 될 가능성이 있지 않을까라는 생각이 들었습니다.
  • 관계가 없는 데이터가 하나의 테이블로 묶이는 느낌이 들었습니다. 주문 신청출고됨이라는 값은 전혀 관계가 없는데 같은 테이블에 포함된 것 같은 느낌을 받았습니다.
  • 잘못된 상태를 사용하게 될 가능성이 존재한다고 생각했습니다. 주문 도메인에서 의도치않게 출고됨이라는 재고의 Status를 사용하게 될 위험이 존재한다고 생각했습니다. (물론 domain_name이라는 변수를 가지기 때문에 이러한 문제가 발생할 가능성은 적다고 생각합니다)
  • 특정 상태를 탐색해서 가져올 때 불필요한 탐색이 진행된다고 생각했습니다. (이 역시 domain_name을 기준으로 index를 활용할 수도 있고, 상태라는 테이블이 많은 값을 가질 가능성은 적어 성능적인 차이가 유의미하지는 않을 거라 생각합니다)
profile
기록하고 정리하는 걸 좋아하는 개발자.

0개의 댓글