UMC

[UMC] DB 정규화와 이중요청 해결

codie0226 2025. 3. 23. 23:58
반응형

DB 정규화


MySQL과 같은 관계형 데이터베이스는 레코드들이 모여 있는 테이블과 테이블들 간의 연관관계로 이루어져 있다.

그런만큼 프로젝트 규모가 커지고 DB 구조가 복잡해질수록 테이블 구조가 잘 연결되어 있어야 데이터를 잘 정리할 수 있고 DB의 성능 또한 저하되지 않는다.

 

정규화는 테이블 간에 중복된 데이터를 허용하지 않는 것을 목표로 두고 진행하며 데이터의 무결성 유지와 DB의 저장 용량을 확보할 수 있다. 정규화는 총 3단계로 이루어지며 지금부터는 이번 주에 진행했던 DB 설계 미션과 함께 살펴보겠다.

 

1주차 미션 DB 설계

 

제 1정규화


제 1정규화는 테이블의 컬럼이 원자값을 갖도록 테이블을 분해하는 것이다. 즉 하나의 레코드에 하나의 컬럼에 대해 하나의 값만 가질 수 있도록 테이블을 분해해야 한다.

 

이번 주차 미션에서 설계해야 하는 기능의 와이어프레임에서는 유저가 본인의 선호 음식을 여러 가지 선택할 수 있게 되어있다. 그렇다면 유저 테이블의 선호 음식 컬럼에 여러 개의 값을 집어넣어도 되는 걸까? 그렇게 DB가 설계된다면 제 1정규화의 조건을 지킬 수 없다.

 

내가 설계한 DB에서는 유저의 선호 음식을 다대다 관계(N:M relation)로 연관지었다. 다른 테이블들에서도 하나의 컬럼에 여러 개의 값이 들어 갈 수 없으므로 제 1정규화를 만족한다.


 

제 2정규화


제 2정규화는 제 1정규화를 진행한 테이블에 대해 완전 함수 종속을 만족하도록 테이블을 분해하는 것이다. 완전 함수 종속이라는 것은 기본키의 부분집합이 결정자가 되어서는 안된다는 것을 의미한다.

 

학번, 강의, 강의실, 성적이라는 컬럼이 있는 테이블에서 (학번, 강의)라는 복합키를 기본키로 묶었을 때, (학번, 강의)는 성적을 결정하고, 강의실은 강의라는 컬럼에 의해 결정된다.

이때 기본키의 부분키인 강의라는 컬럼이 결정자이기 때문에 이러한 경우 기존의 테이블에서 강의실을 분해하여 (학번, 강의, 성적)의 테이블과 (강의, 강의실)이라는 새로운 테이블로 관리할 수 있다.

 

이번 주 미션의 DB를 살펴보자. 상점 테이블을 보면 뭔가 위태로운 모습이라는 것을 알 수 있을 것 같다. 일단 기본적으로 다대다 중간다리용 연결 테이블을 제외하고는 기본키로 id라는 컬럼을 각 테이블에서 사용하고 있다. 복합키를 사용하는 것은 user_food_category라는 테이블과 user_area_mission이라는 테이블밖에 없다. 현재는 해당 테이블들에서 기본키의 부분집합이 다른 컬럼의 결정자가 되는 모습은 보이지 않는다.

그런데 만약 user_area_mission이라는 테이블에 username이라는 커럼이 있었다면 어떨까? 그렇게 된다면 username은 id2가 결정할 수 있게 되고, 제 2정규화를 만족할 수 없을 것이다.


제 3정규화

제3 정규화란 제2 정규화를 진행한 테이블에 대해 이행적 종속을 없애도록 테이블을 분해하는 것이다. 이행적 종족이란 A -> B, B -> C가 성립할 때 A  -> C가 성립되는 것을 의미한다.

 

이번에도 (학번, 강의, 수강료)라는 테이블에 대해 생각해보자. 학번은 강의를 결정하고, 강의는 수강료를 결정하고 있을 것이다. 결국 학번으로도 수강료를 결정하게 되는 결과를 초래한다. 이러한 이행적 종속을 해결하기 위해서는 (학번, 강의)테이블과 (강의, 수강료)라는 테이블로 분리해야 한다.

 

이번 주 미션에서는 하나의 예시를 찾아 볼 수 있을 것 같다.

기능에 따라 설명이 달라질 수 있는 테이블이다.

각 미션마다 포인트가 달라지고, 모든 유저와 상점에 대해 개별로 포인트가 지급되는 방식이면 별 문제가 없는 테이블일 것이다.

그러나 상점마다 지급되는 포인트가 정해져 있다면? 유저마다 지급되는 포인트가 정해져 있다면? 그렇다면 포인트라는 컬럼을 따로 빼서 관리하는 것이 더 효과적일 것이다. 왜냐하면 포인트가 달라져서 데이터를 수정해야 하는 순간이라면, 비정규화 상태에서 모든 레코드에 대해서 수정하는 것보다는 개별의 테이블에서 한번만 수정하는 것이 더 효율적이기 때문이다.


BCNF 정규화

BCNF 정규화는 제 3정규화를 진행한 테이블에 대해 모든 결정자가 후보키가 되도록 하는 것이다. 곧 결정자라면 후보키에 속해있어야 한다는 것이다.

 

다시 (학번, 강의, 교수)라는 테이블에 대해 예시를 들어보자. (학번, 강의)가 기본키가 될 것이고 기본키가 교수를 결정하고 있다. 하지만 이 경우에서는 교수가 강의를 결정하고 있는 상황이지만, 후보키에 속해있지 않다. 따라서 BCNF 정규화를 만족시키기 위해서는 (학번, 교수)와 (강의, 교수)라는 테이블로 분해해야 한다.

 

이번 주 미션에서는 후보키가 아니지만 결정자의 역할을 할 수 있는 컬럼이 있는 테이블이 보이지 않는다.


DB의 이중요청에 대한 핸들링

가끔 서버에 API를 요청할 때 어떠한 외부 조작(알람, 전화 등)이나 오류(인터넷 끊김 등), 또는 버그(무한루프 코드 등)로 인해 같은 요청이 여러번 일어나는 경우가 잦다. 실제로 저번 프로젝트에서 프론트-백 연동과정에서 심심찮게 일어나던 오류이다. (같은 요청으로 한 테이블에 레코드 1400개가 만들어지기도 했다...)

 

해결방안

1. 레포지토리 코드 상에서 중복 감지

저번 프로젝트에서 내가 사용했던 방법이었다... 하지만 그 때는 상황이 긴박해서 그다지 효율적인 코드라고 할 수는 없을 것 같다.

export const challengeExist = async (userId: bigint): Promise<Boolean> => {
  const currentTime = new Date(); //현재 시간
  const minute: number = currentTime.getMinutes() > 30 ? 30 : 0;
  currentTime.setMinutes(minute, 0, 0); //시간 초기화
  const nextTime = new Date();
  nextTime.setMinutes(currentTime.getMinutes() + 30, 0, 0);

  const isExistChallenge = await prisma.challenge.findFirst({
    where: {
      userId: userId,
      createdAt: {
        lt: new Date(nextTime.toUTCString()),
        gte: new Date(currentTime.toUTCString())
      }
    }
  });

  if(isExistChallenge){
    return true;
  }
  else{
    return false;
  }
};

위 함수는 repository.ts에서 이미 생성되어 있는 챌린지를 인식하는 함수이다. 테이블에서 일정한 시간(위 코드상으로는 30분) 내에 생성된 챌린지 있다면 true를 반환하는 방식이다.

이러한 방식의 장점은 내가 원하는 시간 스코프에서 중복을 감지할 수 있다는 것이다. 단점으로는 이중요청을 처리하기 위해 DB에 요청 쿼리를 보내야 한다는 뜻이고, DB의 규모에 따라 시간이 훨씬 더 걸릴 수 있다.

 

2. UPDATE인 경우 row affected 확인해보기

인터넷에서 찾아본 방식이다.

UPDATE 쿼리를 사용하면 결과에서 ~~row affected로 반환된다. SELECT 문을 확인하지 않고도 ROW_COUNT() DBMS 함수로 확인하면 더 효율적으로 관리할 수 있다.

 

3. 트랜잭션 사용하기

은행 업무처리와 같은 곳에서 많이 사용하는 방식이다. 일련의 요청을 묶어서 트랜잭션으로 동시성을 해결하는 것이다.

 

느낀점

DB 설계를 할 때에는 개발 중에 수정하기 어렵기 때문에 처음 설계할 때 최대한 최적화를 시켜서 하는 것이 핵심이라고 생각한다. 그런 의미에서 우리가 비즈니스 로직에 따라 DB를 설계하면, 정규화 단계를 무심코 지나가는 경우가 있고, 나중에 DB를 갈아엎는 수준까지의 오류가 발생한다면 그때가서는 너무 늦은 것이다. 그만큼 정규화 단계를 직접 실습해보면서 고민해보고, 테이블을 효율적으로 분리할 수 있어야 나중에 개발할 때에도 실수 없이 join할 수 있을 것이다.

또한 프론트에서 요청을 보낼 때 고질적으로 발생하는 이중 요청에 대해서도 여러가지 해답을 얻을 수 있었다. 협업을 하다 보면 굉장히 자주 일어나는 일이고, 프론트 - 백 연동 작업을 더 빠르고 효율적으로 하기 위해서는 이중 요청이 발생하기 전에 백엔드 측에서 이러한 부분을 해결하고 미연에 방지하는 센스가 필요할 것이다.

반응형

'UMC' 카테고리의 다른 글

[UMC] SQL Query  (0) 2025.03.30