Prisma의 트랜잭션은 여러개의 쿼리를 하나의 트랜잭션으로 수행할 수 있는 Sequential 트랜잭션과 Prisma가 자체적으로 트랜잭션의 성공과 실패를 관리하는 Interactive 트랜잭션이 존재한다.

 

Sequential 트랜잭션

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// Sequential 트랜잭션은 순차적으로 실행된다
// 결과값은 각 쿼리의 순서대로 배열에 담겨 반환된다
const [posts, comments] = await prisma.$transaction([
  prisma.posts.findMany(),
  prisma.comments.findMany(),
]);

 

Sequential 트랜잭션은 Prisma의 여러 쿼리를 배열( [ ] )로 전달받아, 각 쿼리들을 순서대로 실행하는 특징이 있다.

이러한 특징은 여러 작업이 순차적으로 실행되어야할 때 사용할 수 있다.

 

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// Prisma의 Interactive 트랜잭션을 실행한다.
const result = await prisma.$transaction(async (tx) => {
  // 트랜잭션 내에서 사용자를 생성한다.
  const user = await tx.users.create({
    data: {
      email: 'testuser@gmail.com',
      password: 'aaaa4321',
    },
  });

  // 에러가 발생하여, 트랜잭션 내에서 실행된 모든 쿼리가 롤백된다.
  throw new Error('트랜잭션 실패!');
  return user;
});

 

또한, Sequential 트랜잭션은 Prisma의 모든 쿼리 메서드뿐만 아니라, Raw Query도 사용할 수 있다.

 

Interactive 트랜잭션

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// Prisma의 Interactive 트랜잭션을 실행합니다.
const result = await prisma.$transaction(async (tx) => {
  // 트랜잭션 내에서 사용자를 생성합니다.
  const user = await tx.users.create({
    data: {
      email: 'testuser@gmail.com',
      password: 'aaaa4321',
    },
  });

  // 에러가 발생하여, 트랜잭션 내에서 실행된 모든 쿼리가 롤백됩니다.
  throw new Error('트랜잭션 실패!');
  return user;
});
  • Interactive 트랜잭션은 모든 비즈니스 로직이 성공적으로 완료되거나 에러가 발생한 경우 Prisma 자체적으로 COMMIT 또는 ROLLBACK을 실행해 트랜잭션을 관리하는 장점을 가지고 있다.
  • Interactive 트랜잭션은 트랜잭션 진행 중에도 비즈니스 로직을 처리할 수 있어, 복잡한 쿼리 시나리오를 효과적으로 구현할 수 있다.
  • $transation() 메서드의 첫번째 인자 async(tx)는 우리가 일반적으로 사용하는 prisma 인스턴스와 같은 기능을 수행한다.

 

[게시판 프로젝트] 회원가입 API에 트랜잭션 적용하기

회원가입 API는 아래와 같은 비즈니스 로직을 가지고 있다.

  1. email, password, name, age, gender, profileImage를 body로 전달받는다.
  2. 동일한 email을 가진 사용자가 있는지 확인한다.
  3. Users 테이블에 email, password를 이용해 사용자를 생성한다.
  4. UserInfos 테이블에 name, age, gender, profileImage를 이용해 사용자 정보를 생성한다.
  5. 회원가입을 완료 처리한다.

여기서 3 사용자 및 4 사용자 정보를 생성하는 과정에서 에러가 발생하게 될 경우 문제가 생길 수 있다.

이를 해결하기 위해, 우리는 트랜잭션 ( Transaction )을 도입할 예정이다.

 

트랜잭션을 도입하면 여러 개의 쿼리를 하나의 작업으로 묶어, 하나의 쿼리가 실패할 경우 전체 트랜잭션을 취소 ( ROLLBACK ) 할 수 있어 데이터의 일관성을 유지할 수 있게 된다.

 

그렇다면, 트랜잭션을 도입해 회원가입 API를 리팩토링 해보자.

 

[게시판 프로젝트] 회원가입 트랜잭션 예시 코드

// src/routes/users.router.js

import { Prisma } from '@prisma/client';

/** 사용자 회원가입 API 트랜잭션 **/
router.post('/sign-up', async (req, res, next) => {
  try {
	  const { email, password, name, age, gender, profileImage } = req.body;
	  const isExistUser = await prisma.users.findFirst({
	    where: {
	      email,
	    },
	  });
	
	  if (isExistUser) {
	    return res.status(409).json({ message: '이미 존재하는 이메일입니다.' });
	  }
	
	  // 사용자 비밀번호를 암호화합니다.
	  const hashedPassword = await bcrypt.hash(password, 10);


    // MySQL과 연결된 Prisma 클라이언트를 통해 트랜잭션을 실행합니다.
    const [user, userInfo] = await prisma.$transaction(
      async (tx) => {
        // 트랜잭션 내부에서 사용자를 생성합니다.
        const user = await tx.users.create({
          data: {
            email,
            password: hashedPassword, // 암호화된 비밀번호를 저장합니다.
          },
        });

        // 트랜잭션 내부에서 사용자 정보를 생성합니다.
        const userInfo = await tx.userInfos.create({
          data: {
            userId: user.userId, // 생성한 유저의 userId를 바탕으로 사용자 정보를 생성합니다.
            name,
            age,
            gender: gender.toUpperCase(), // 성별을 대문자로 변환합니다.
            profileImage,
          },
        });

        // 콜백 함수의 리턴값으로 사용자와 사용자 정보를 반환합니다.
        return [user, userInfo];
      },
      {
        isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted,
      },
    );
    return res.status(201).json({ message: '회원가입이 완료되었습니다.' });
  } catch (err) {
	next(err);
  }
});

 

 

Prisma에서 격리 수준 설정

Prisma의 격리수준은 트랜잭션을 생성할 때, isolationLevel 옵션을 정의함으로써 설정할 수 있다.

import { Prisma } from '@prisma/client';

await prisma.$transaction(
  async (tx) => { ... },
  {
    isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted,
  },
);

 

격리 수준 ( Isolation Level )을 설정할 때, 현재 구현하려는 API에는 어떠한 격리 수준이 필요한지 명확하게 이해해야 한다. 이를 통해 효율적인 데이터베이스의 설계를 할 수 있고, 데이터의 일관성이 깨지지 않도록 구현할 수 있게 된다.

회원 가입 API는 결제시스템과 같이 높은 수준의 일관성을 요구하지 않기 때문에,

READ_COMMITED 격리 수준을 사용했다. 

 


 

사용자 히스토리 ( UserHisotires ) 테이블 생성

이 테이블은 사용자의 정보가 변경될 때마다 변경 내역을 로깅 ( Logging )하기 위해 사용한다.

사용자 정보 변경 API를 구현하면서, 이 변경 내역을 사용자 히스토리 테이블에 함께 데이터를 생성하도록 구현해보자.

 

schma.prisma에 UserHistories model을 생성한다.

// schema.prisma

model Users {
  userId    Int      @id @default(autoincrement()) @map("userId")
  email     String   @unique @map("email")
  password  String   @map("password")
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  userInfos     UserInfos? // 사용자(Users) 테이블과 사용자 정보(UserInfos) 테이블이 1:1 관계를 맺습니다.
  posts         Posts[] // 사용자(Users) 테이블과 게시글(Posts) 테이블이 1:N 관계를 맺습니다.
  comments      Comments[] // 사용자(Users) 테이블과 댓글(Comments) 테이블이 1:N 관계를 맺습니다.
  userHistories UserHistories[] // 사용자(Users) 테이블과 사용자 히스토리(UserHistories) 테이블이 1:N 관계를 맺습니다.

  @@map("Users")
}

model UserHistories {
  userHistoryId String   @id @default(uuid()) @map("userHistoryId")
  userId        Int      @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
  changedField  String   @map("changedField") // 변경된 필드명
  oldValue      String?  @map("oldValue") // 변경 전 값
  newValue      String   @map("newValue") // 변경 후 값
  changedAt     DateTime @default(now()) @map("changedAt")

  // Users 테이블과 관계를 설정합니다.
  user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)

  @@map("UserHistories")
}

 

Prisma db push 명령어를 이용해 동기화 한다.

# schema.prisma에 정의된 모델 정보를 DB와 동기화합니다.
npx prisma db push

 

UUID ( 범용 고유 식별자 ) 

 

UUID ( Universally Unique Identifier, 범용 고유 식별자 )는 총 4개의 정보를 하이픈 ( - )으로 구분해 순차적으로 저장한 데이터 타입이다. 시간 정보를 포함하고 있어 생성된 순서대로 정렬이 되는 특징을 가지고 있다.

 

https://ko.wikipedia.org/wiki/%EB%B2%94%EC%9A%A9_%EA%B3%A0%EC%9C%A0_%EC%8B%9D%EB%B3%84%EC%9E%90

 

범용 고유 식별자 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 범용 고유 식별자(汎用固有識別子, 영어: universally unique identifier, UUID)는 소프트웨어 구축에 쓰이는 식별자 표준으로, 개방 소프트웨어 재단(OSF)이 분산 컴퓨팅

ko.wikipedia.org

 

UUID 디코드 사용해보기

https://www.uuidtools.com/decode

 

UUID Decoder | UUIDTools.com

How to decode a UUID Embedded in every UUID is the version and variant of the UUID. Other information such as the time the UUID was generated can also be extracted in some cases. The tool above extracts this information automatically. The UUID version is r

www.uuidtools.com

 

사용자 히스토리 ( UserHistories ) 테이블은 사용자 정보에 대한 모든 변경 내역을 기록한다. 추후에 API 호출 히스토리와 같은 추가 정보를 기록하는 다른 히스토리 테이블을 만들면, 더욱 다양한 데이터를 수집할 수 있다.

사용자 히스토리 테이블은 일반적인 다른 테이블과는 조금 다르게 설계해야한다.

Integer 타입의 기본키나 createdAt, updatedAt과 같은 컬럼을 사용하기보다는 UUID를 사용해 컬럼 수를 최소화 하는 것이 로그 테이블에서 더욱 효율적인 설계가 된다.

 


[게시판 프로젝트] 사용자 정보 변경 API

[게시판 프로젝트] 사용자 정보 변경 API 비즈니스 로직

  1. 클라이언트가 로그인된 사용자인지 검증한다.
  2. 변경할 사용자 정보 name, age, gender, profileImage를 body로 전달받는다.
  3. 사용자 정보 ( UserInfoes ) 테이블에서 사용자의 정보들을 수정한다.
  4. 사용자의 변경된 정보 이력을 사용자 히스토리 ( UserHistories ) 테이블에 저장한다.
  5. 사용자 정보 변경 API를 완료한다.

사용자 정보 변경 API는 3번 사용자 정보의 수정4번 사용자 히스토리 데이터 삽입, 2개의 비즈니스 로직을 하나의 작업으로 처리해야한다.

비즈니스 로직을 수행하는 도중 오류가 발생할 경우, 데이터의 일관성이 깨지게 될 수 있다.

이렇게 되면, User History Table의 데이터들을 믿을 수 없게 되는 상황이 발생하게 된다.

 

[게시판 프로젝트] 사용자 정보 변경 API

// src/routes/users.router.js

/** 사용자 정보 변경 API **/
router.patch('/users/', authMiddleware, async (req, res, next) => {
  try {
    const { userId } = req.user;
    const updatedData = req.body;

    const userInfo = await prisma.userInfos.findFirst({
      where: { userId: +userId },
    });

    await prisma.$transaction(
      async (tx) => {
        // 트랜잭션 내부에서 사용자 정보를 수정합니다.
        await tx.userInfos.update({
          data: {
            ...updatedData,
          },
          where: {
            userId: userInfo.userId,
          },
        });

        // 변경된 필드만 UseHistories 테이블에 저장합니다.
        for (let key in updatedData) {
          if (userInfo[key] !== updatedData[key]) {
            await tx.userHistories.create({
              data: {
                userId: userInfo.userId,
                changedField: key,
                oldValue: String(userInfo[key]),
                newValue: String(updatedData[key]),
              },
            });
          }
        }
      },
      {
        isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted,
      },
    );

    return res
      .status(200)
      .json({ message: '사용자 정보 변경에 성공하였습니다.' });
  } catch (err) {
    next(err);
  }
});

'데이터베이스' 카테고리의 다른 글

[DATABASE] 트랜잭션 ( Transaction )  (1) 2024.09.11
[DATABASE] ORM과 Prisma  (0) 2024.09.06
[DATABASE] Raw Query  (0) 2024.09.06
[DATABASE] SELECT JOIN 연산자  (0) 2024.09.05
[DATABASE] SQL 제약조건  (0) 2024.09.05

트랜잭션 ( Transaction )은 작업의 완전성을 보장해주기 위해 사용되는 개념이다.

특정한 작업을 전부 처리하거나, 전부 실패하게 만들어 데이터의 일관성을 보장해주는 기능이다.

 

트랜잭션 ( Transaction )을 사용하는 대표적인 이유는 작업의 단위를 하나의 쿼리에 종속하는 것이 아닌,

여러개의 작업 ( 쿼리 )을 묶어 하나의 작업 단위로 그룹화해 처리하는 작업을 뜻한다.

 

대부분의 데이터베이스가 트랜잭션의 특징을 이용하고 있으며, MySQL뿐만 아니라 AWS, MongoDB 등 다양한 데이터베이스에서도 트랜잭션의 개념을 확인할 수 있다.

 

트랜잭션의 사용 이유

트랜잭션을 이용해 다양한 문제 상황들을 해결할 수 있다.

예를 들어 은행에서 계좌이체를 하게 될 경우 아래와 같은 상황이 발생했다고 해보자.

 

계좌이체 비즈니스 로직

  1. A 고객의 계좌에서 1000원을 차감한다.

  2. B 고객의 계좌에서 1000원을 추가한다.

 

여기서 1번 작업 이후 2번 작업을 수행하던 중 에러가 발생하게 되면 A 고객의 계좌에서 1000원만 차감되기만 하는 문제가 생긴다. 만약 순서가 반대로 되면, B 고객의 계좌에만 1000원이 증가되는 문제가 발생한다.

이런 부분 업데이트 ( Partial Update ) 와 같은 상황을 방지하기 위해 트랜잭션 ( Transction )이라는 개념을 도입하게 됐다.

 

단순히 위와 같은 상황 외에도 작업한 내역을 저장하는 로깅작업, 영화관의 예매 시스템, 은행의 결제 시스템 등 데이터의 일관성을 유지해야하는 다양한 상황에서 사용한다.

 

결국, 트랜잭션 ( Transaction )을 이용하면 사용자가 항상 어플리케이션 실행을 완료하도록 구성할 수 있게되고, 실행을 중단할 만한 치명적인 오류가 발생하더라도, DB에 피해가 가지않아 더욱 안전하게 어플리케이션을 구성할 수 있게 된다.

 

트랜잭션의 특징 ( ACID )

트랜잭션의 특징 ( ACID )은 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 특징들을 나열해 놓은 개념이다.

  • ACID는 트랜잭션을 이용해 데이터베이스를 더욱 안전하게 구성할 수 있게 도와주는 트랜잭션 특징 4가지의 맨 앞단어를 하나씩 가져와 만든 것이다.

 

ACID는 트랜잭션의 4가지 특징을 나타낸다.

  • 원자성 ( Atomicity )
  • 일관성 ( Consistency )
  • 격리성 ( Isolation )
  • 지속성 ( Durability )

원자성 ( Atomocity )

원자성 ( Atomocity )은 트랜잭션 내에서 실행되는 명령들을 하나의 묶음으로 처리해, 내부에서 실행된 명령들이 전부 성공하거나, 아니면 모두 실패해야한다는 특징을 말한다.

 ▶ 여기서, "원자성"이란, 나눠질 수 없는 단일 작업이라는 것을 의미한다.

 

원자성 ( Atomicity )은 트랜잭션의 가장 대표적인 특징이다. 트랜잭션에서 실행되는 쿼리마다 하나의 작업 단위로 보지않고, 여러개의 작업들을 묶어 하나의 작업단위로 보게된다.

 

트랜잭션을 사용하면 앞서 예시로 든 2단계로 구분된 비즈니스 로직인 고객의 계좌금액 차암과 금액 증가를 하나의 계좌이체라는 기능으로 묶어서 관리할 수 있게 되는 것이다.

결국, 원자성이라는 특징을 이용해 우리는 각각의 쿼리를 별도로 실행하는 것이 아니라, 동시에 실행해야하는 여러개의 쿼리를 묶어서 관리할 수 있게 되는 것이다. 이렇게 하면, 여러개의 비즈니스 로직을 상세하게 알지 않더라도 트랜잭션을 활용해 비즈니스 로직을 전체적으로 관리할 수 있게 된다.

 

일관성 ( Consistency )

일관성 ( Consistency )은 트랜잭션 내부에서 처리되는 데이터의 일관성을 유지해야하는 특징이다.

만약 작업이 성공할 경우 아무런 문제가 발생하지 않고, 실패하더라도 작업을 진행하던 도중 실패한 상태로 데이터를 방치하지 않는 특징을 지닌다.

 

일관성 ( Consistency )의 특징은 트랜잭션 내의 데이터는 일관되어야하며, 에러가 발생하더라도 데이터의 상태가 일관성을 유지해야 한다는 특징을 지닌다.

만약 일관성이 지켜지지 않을 경우 우리는 트랜잭션을 이용하더라도 언제 데이터가 파손될지 모르는 불안감을 가진 체 작업을 해야한다.

 

강의 등록의 비즈니스로직

  1. 강의 ( Coureses ) 테이블에서 강의를 생성한다.
  2. 생성된 강의 데이터를 참조하는 강의 목록 ( CourseUnit ) 테이블에 강의 영상을 업로드 한다.
  3. 강의 목록 ( CourseUnit ) 테이블에 모든 강의 영상을 업로드 했으면 COMMIT

첫번째 강의 테이블에 강의를 생성하는것은 성공했지만 두번째 강의 목록 테이블에서 모든 강의 영상을 삽입하는데 실패했다면, ROLLBACK이 실행되어 강의( Courses ) 테이블과 강의 목록 ( CourseUnit ) 테이블에는 트랜잭션 시작 전 상태로 복구된다.

이렇게, 데이터는 강의 자체가 생성되지 않거나, 모든 강의가 업로드 되는 상황이 발생하게 되어야, 일관성을 유지할 수 있을 것이고, 데이터베이스를 믿고 작업을 진행할 수 있게 된다.

 

격리성 ( Isolation )

격리성 ( Isolation )은 트랜잭션이 실행 중인 경우 다른 트랜잭션에 의해 데이터가 변경되는 것을 방지하는 특징이다.

트랜잭션이 완전히 수행되거나 완전히 수행되지 않은 상태를 외부에서 참조할 수는 있지만, 트랜잭션의 중간 과정이나 중간 결과를 볼 수 없도록 하는 특징이다.

 

격리성 ( Isolation )의 경우, MySQL에서는 사용중인 DB 오브젝트에 락 ( Lock )을 걸어 격리성을 구현하게 된다. 여기서 락 ( Lock )을 건 상태는 DB에 접속한 또다른 클라이언트가 해당 DB 오브젝트를 읽거나, 사용할 수 없도록 방지해, 데이터 무결성을 보장한다.

  ▶ 격리성이란 특징에서 동시성 ( Concurrency )과 격리 수준 ( Isolation Level )이라는 두 가지 중요한 개념이 나타났다.

 

동시성 ( Concurrency )이란 무엇일까?

동시성 ( Concurrency )은 여러 클라이언트가 동시에 하나의 데이터를 사용 및 공유 하는 것을 말한다.

동시성은 다수의 사용자가 동일한 시스템을 공유하면서 발생하는 동시 접근 문제를 해결해야 한다.

 

동시성 문제 ( Concurrency Issue )는 여러 클라이언트가 동시에 같은 데이터를 접근하려고 할때 발생한다.

예를 들어 2개의 계좌이체가 발생한다고 가정해보자.

  1. 1 트랜잭션에서 A 계좌에 천원을 차감해 10000 → 9000 으로 수정했다.
  2. 2 트랜잭션에서 아직 커밋 되지 않은 1 트랜잭션의 데이터를 바탕으로 A 계좌에서 천원을 차감해 9000 → 8000으로 수정했다.
  3. 1 트랜잭션에서 에러가 발생해, 트랜잭션을 ROLLBACK 하게 되었다. 따라서, A 계좌의 잔고는 원래대로 10000이 되었다.
  4. 그런데 2 트랜잭션에서 잘못된 데이터를 바탕으로 작업을 수행한 후 트랜잭션을 COMMIT 하게 되었다. 이로 인해 A 계좌의 잔고가 8000으로 잘못 표시되는 문제가 발생했다.

위 예시처럼 동시에 여러 트랜잭션이 동일한 데이터에 접근할 때는 데이터의 일관성을 유지하기 어려울 수 있다.

이러한 문제를 해결하기 위해, 자원을 사용하는 하나의 클라이언트만 해당 자원을 점유할 수 있도록 하여, 다른 사용자가 접근할 수 없도록 만들어 자원을 공유하는 원인을 제거하면 된다. 이것을 자원 잠금 ( Resource Locking )라고 부르며,

락 ( Lock )이라는 개념이 나오게 된다.

락 ( Lock )은 하나의 트랜잭션에서 사용중인 데이터를 잠그는 방식으로, 다른 트랜잭션들이 그 데이터에 접근하지 못하도록 한다. 이 방식을 사용하면, 어떤 트랜잭션도 다른 트랜잭션의 중간 상태를 볼 수 없게 되므로 데이터 일관성을 유지할 수 있게 된다.

 

지속성 ( Durability )

지속성 ( Durability )은 트랜잭션이 성공적으로 커밋된 후, 해당 트랜잭션에 의해 생성 또는 수정된 데이터가 어떠한 상황에서도 보존되는 특징이다. 다시 말해, 트랜잭션이 완료되면 결과는 데이터베이스에 영구적으로 저장되고, 이후 시스템에 어떠한 문제가 생기더라도 데이터는 손상되지 않는다.

 

지속성 ( Durability )은 트랜잭션의 안전성을 보장하며, 데이터 손실 없이 시스템의 안전성을 유지하는데 중요한 역할을 담당한다.

트랜잭션이 성공적으로 완료되면, 해당 트랜잭션에 의해 생성 또는 변경된 데이터는 데이터베이스에 COMMIT 명령을 통해 영구적으로 저장된다. 하지만, 트랜잭션 수행 도중 시스템이 비정상 종료되더라도,

시스템은 트랜잭션 로그 ( Transaction Log )를 통해 아직 커밋되지 않은 트랜잭션을 복구할 수 있다.

 


트랜잭션 실습

MySQL의 트랜잭션 살펴보기

  •  MySQL에서 트랜잭션은 아래와 같은 명령어로 사용한다.
-- 트랜잭션을 시작합니다.
START TRANSACTION;

-- 성공시 작업 내역을 DB에 반영합니다.
COMMIT;

-- 실패시 START TRANSACTION이 실행되기 전 상태로 작업 내역을 취소합니다.
ROLLBACK;

 

트랜잭션을 사용하는 문법을 확인해봤으니,

MySQL에서 트랜잭션을 사용하기 위한 간단한 예시를 실행해보자.

 

MySQL의 트랜잭션 예시코드

-- SPARTA 테이블을 생성한다.
CREATE TABLE IF NOT EXISTS SPARTA
(
    spartaId      INT(11)      NOT NULL PRIMARY KEY AUTO_INCREMENT,
    spartaName    VARCHAR(255) NOT NULL,
    spartaAddress VARCHAR(255) NOT NULL
);

-- 1번째 트랜잭션을 실행한다.
START TRANSACTION;

-- SPARTA 테이블에 더미 데이터 3개를 삽입한다.
INSERT INTO SPARTA (spartaName, spartaAddress)
VALUES ('SPARTA1', 'SEOUL'),
       ('SPARTA2', 'BUSAN'),
       ('SPARTA3', 'DAEGU');

-- 1번째 트랜잭션을 DB에 적용한다.
COMMIT;


-- 2번째 트랜잭션을 실행한다.
START TRANSACTION;

-- SPARTA 테이블에 더미 데이터 3개를 삽입한다.
INSERT INTO SPARTA (spartaName, spartaAddress)
VALUES ('SPARTA4', 'SEOUL'),
       ('SPARTA5', 'BUSAN'),
       ('SPARTA6', 'DAEGU');

-- 2번째 트랜잭션을 롤백한다.
ROLLBACK;

-- 테이블의 상태를 확인한다.
SELECT * FROM SPARTA;

 

 

예시 코드를 실행하면, 2번째 트랜잭션에서 수행하는 INSERT INTO가 ROLLBACK 되었기 때문에 실제 SPARTA 테이블은 6개가 아닌, 3개의 데이터만 삽입되어 있는 상태로 존재한다.

 

위 예시코드는 단순히 트랜잭션의 COMMIT과 ROLLBACK이 어떤식으로 동작하는지를 확인하기 위해 사용해봤다.


락 ( LOCK )

락 ( Lock )은 동시성을 제어하기 위해 사용하는 기능이다. 해당하는 데이터를 점유해 다른 트랜잭션의 접근을 막아 동시성과 일관성의 균형을 맞추기 위해 사용한다.

 

하나의 데이터를 여러 사용자들이 동시에 변경하려고 할 때, 락이 존재하지 않다면, 한번에 여러 번의 수정이 발생하게되고, 최종 수정된 결과값을 인지할 수 없게 되는 상황으로 인해 데이터베이스의 일관성이 깨지게 된다. 이런 상황을 방지하기 위해 데이터베이스에서는 락 ( Lock )이라는 기능을 지원하게 되었다.

 

락 ( Lock )의 종류

  ● 공유 락 ( Shared Locks ) | 읽기 락 ( READ Locks )

  • 다른 트랜잭션이 데이터를 읽는 것은 허용하지만, 수정하는 것을 금지한다.
  • READ 전용 락이라고 불리기도 하며, 해당 락을 사용하는 트랜잭션이 모든 작업을 수행했다면 공유 락은 해제된다.

예시 SQL

# 트랜잭션을 시작합니다.
START TRANSACTION;

# SPARTA 테이블을 조회할 때, 해당 데이터들에 공유 락을 설정합니다.
SELECT * FROM SPARTA LOCK IN SHARE MODE;

 

  ● 배타 락 ( Exclusive Locks ) | 쓰기 락 ( WRITE Locks )

  • 다른 트랜잭션이 데이터를 읽거나, 수정하는 것을 금지한다.
  • WRITE 전용 락이라고 불리며, 트랜잭션이 해당하는 데이터를 점유한 후 다른 트랜잭션이 해당 데이터에 접근 할 수 없도록 만든다.

예시 SQL

# 트랜잭션을 시작합니다.
START TRANSACTION;

# SPARTA 테이블을 조회할 때, 해당 데이터들에 배타 락을 설정합니다.
SELECT * FROM SPARTA FOR UPDATE;

 

 

락킹 수준 ( Locking Level )

  ● 글로벌 락 ( Global Locks ) | 데이터베이스 락 ( Database Locks )

  • 데이터베이스의 모든 테이블에 락을 걸어, 현재 트랜잭션을 제외한 나머지 트랜잭션들이 모든 테이블을 사용할 수 없도록 만든다.
  • 가장 높은 수준의 락을 가지고 있으며, 가장 큰 범위를 가지고 있다.

예시 SQL

# 글로벌 락을 획득합니다.
# MySQL 서버에 존재하는 모든 테이블에 락을 겁니다.
FLUSH TABLES WITH READ LOCK;

 

  ● 테이블 락 ( Table Locks )

  • 다른 사용자가 작업중인 테이블을 동시에 수정하지 못하도록 한다.

예시 SQL

# SPARTA 테이블에 테이블 락을 설정합니다.
LOCK TABLES SPARTA READ;

 

  ● 네임드 락 ( Named Locks )

  • 테이블이나 테이블의 행과같은 DB 오브젝트가 아닌, 특정한 문자열을 점유한다.

예시 SQL

# sparta_name 문자열을 획득합니다.
# 만약, 10초 동안 획득 하지 못한다면, NULL을 반환합니다.
SELECT GET_LOCK('sparta_name', 10);

 

  ● 메타데이터 락 ( Metadata Locks )

  • 다른 사용자가 작업중인 테이블의 동일한 행 및 동일한 데이터베이스의 객체를 동시에 수정하지 못하도록 한다.

예시 SQL

# 테이블 구조를 변경할 때, MySQL은 내부적으로 메타데이터 락을 설정합니다.
ALTER TABLE SPARTA ADD COLUMN Age Int;

 

 

락은 다양한 락킹 수준 ( Locking Level )을 가지고 있는데, 잘못된 락 설정을 하게 될 경우 우리는 모든 API가 동작하지 않는 교착 상태 ( Dead Lock )가 발생하게 되어, 프로그램이 멈춰버리는 문제가 발생하게 될 수 있다.

 

교착 상태 ( Dead Lock ) 예시

교착 상태 ( Dead Lock )는 여러 테이블에 락 ( Lock )을 적용해, 다른 작업이 처리되지 못하게 점유하고 있는 작업이 있을 때, 다른 작업을 끝나는 것을 무한정 기다리는 것을 나타낸다.

 

아래와 같은 2개의 트랜잭션이 동시에 실행된다고 가정해보자.

  1. A ▶ B 테이블을 순차적으로 사용하는 트랜잭션
  2. B ▶ A 테이블을 순차적으로 사용하는 트랜잭션

가장 먼저 1은 처음 A 테이블을 점유하기 위해 락을 건다. 그리고 동시에 2 또한 B 테이블을 점유하기 위해 락을 건다.

그러면 1은 B 테이블의 락이 풀리기를 기다리는 상태가 발생하고, 2 또한 A 테이블의 락이 풀리기를 기다리는 상태가 발생하게 된다. 이처럼 리소스를 접근하려고 할 때, 서로가 서로의 리소스를 점유하고 있을 때 발생하는 것을 교착 상태 ( Dead Lock )이라고 부른다.

해당하는 상황을 해결하기 위해 우리는 트랜잭션에서 사용하는 락 ( Lock )의 수준을 명확하게 이해하고, 적재적소에 필요한 락의 수준을 설정해 트랜잭션을 구성해야한다.


트랜잭션의 격리 수준 ( Isolation Level )

트랜잭션의 격리 수준 ( Isolaction Level )은 여러 트랜잭션이 동시에 처리될 때 다른 트랜잭션에서 변경 및 조회하는 데이터를 읽을 수 있도록 허용하거나 거부하는 것을 결정하기 위해 사용하는 것이다.

▶ 여기서, 중요한 점은 '데이터의 일관성''동시성 처리 성능' 사이에서 균형을 잡는 것이다.

 

트랜잭션의 격리 수준은 대표적으로 4가지로 나타낸다.

 

READ UNCOMMITTED

  • 커밋 되지 않은 읽기 ( Uncommitted Read )를 허용하는 격리 수준이다.
  • 가장 낮은 수준의 격리수준이며, 락을 걸지 않아 동시성이 높지만 일관성이 쉽게 깨질 수 있다.

READ COMMITTED

  • 커밋 된 읽기 ( Committed Read )만을 허용하고, SELECT 문을 실행할 때 공유락을 건다.
  • 다른 트랜잭션이 데이터를 수정하고 있는 중에 데이터를 읽을 수 없어 커밋되지 않은 읽기현상이 발생하지 않는다.

REPEATABLE READ

  • 읽기를 마치더라도 공유락을 풀지 않으며, 트랜잭션이 완전히 종료될 때 까지 락을 유지한다.
  • 공유락이 걸린 상태에서 데이터를 수정하는 것은 불가능하지만, 데이터를 삽입하는 것이 가능해진다. 그로인해 팬텀 읽기가 발생할 수 있는 문제점이 있다.

SERIALIZABLE

  • 데이터를 읽는 동안 다른 트랜잭션이 해당 데이터를 읽거나 삽입할 수 없고, 새로운 데이터를 추가하는 것 또한 불가능하다.
  • 가장 높은 수준의 격리 수준이므로, 동시성이 떨어지는 문제점이 존재한다.

 

커밋되지 않은 읽기 ( Uncommitted Read )

커밋되지 않은 읽기 ( Uncommitted Read )는 다른 트랜잭션에 의해 작업중인 데이터를 읽게 되는 것을 나타낸다. 만약 커밋되지 않은 읽기가 발생할 경우, 의도치 않은 데이터를 참조하게 되어 데이터의 일관성이 깨지게 되는 상황이 발생하게된다.

 

팬텀 읽기 ( Phantom Read )

트랜잭션을 수행하던 중 다른 트랜잭션에 의해 삭제된 데이터를 팬텀행 ( Phantom Rows )이라고 한다. 여기서, 팬텀행에 해당하는 데이터를 읽는 것을 팬텀 읽기 ( Phantom Read )라고 부른다.

Prisma 라이브러리 설치

# yarn 프로젝트를 초기화합니다.
yarn init -y

# express, prisma, @prisma/client 라이브러리를 설치합니다.
yarn add express prisma @prisma/client

# nodemon 라이브러리를 DevDependency로 설치합니다.
yarn add -D nodemon

# 설치한 prisma를 초기화 하여, prisma를 사용할 수 있는 구조를 생성합니다.
npx prisma init

 

위 명령문을 통해 Prisma를 설치할 수 있다.

  • prisma는 Prisma를 터미널에서 사용할 수 있도록 도구를 설치하는 패키지다
  • @prisma/client는 Node.js에서 Prisma를 사용할 수 있게 해준다.
  • nodemon은 개발 코드가 변경되었을 때 자동으로 서버를 재시작 해주는 패키지다.

 

npx prisam init

내 프로젝트 폴더 이름
├── prisma
│   └── schema.prisma
├── .env
├── .gitignore
├── package.json
└── yarn.lock

 1. prisam 폴더 안에 prisma.schema 파일 생성

  • 이 파일은 Prisma가 사용할 데이터베이스를 설정하기 위해사용하는 파일이므로 지우면 안된다.

 2. root 폴더에 .env 파일 생성 

  • 이 파일은 외부에 공유되어선 안되는 비밀 정보들이 저장되어 있는 파일이다.

 3. root 폴더에 .gitignore 파일 생성

  • .env 파일이 git에 업로드 되지 않도록 설정 되어 있다.

여기서 생성된 폴더나 파일들을 임의로 옮기지 않아야 한다. Prisam는 정해진 경로에 있는 파일을 사용하고 저장하기 때문에 임의로 옮기면 오동작 할 가능성이 높다.

 

nodemon 라이브러리

파일을 저장할 때마다 변경 사항을 감지하고, 자동으로 서버를 재시작해 주는 라이브러리다. 개발 중 변경사항을 즉시 반영해 개발 효율성을 향상시킬 수 있다.

 

앞서 매번 코드를 수정하거나 서버에서 에러가 발생해 종료되었을 때, 매번 서버를 재시작해야했다.

이전에는 이를 위해 node app.js 명령어를 이용해 서버를 수동으로 재시작했는데, 이런 방법은 너무 번거로운 작업이다.

이런 상황을 nodemon 이라는 도구를 이용해 해결할 수 있다.

 

▶ nodemon 명령어

  • nodemon 으로 서버를 실행하기 위해서 아래와 같이 사용한다.
# 형식
nodemon <실행할 JavaScript 파일명>

# nodemon을 이용해 app.js 파일 실행하기
nodemon app.js

 

단순히 터미널에 명령어를 사용하는 것 뿐 아니라, package.json 에 nodemon 을 이용해 서버를 실행하는 스크립트 ( scripts )를 등록하면, 매번 명령어를 입력하지 않아도 간편하게 서버를 시작할 수 있다.

 

아래와 같이 package.json을 수정

// package.json

...

"scripts": {
	"dev": "nodemon app.js"
},

 

터미널에서 yarn run dev 명령어를 실행하면, nodemon을 이용해 서버를 시작할 수 있게 된다.


schema.prisma

Prisma가 사용할 데이터베이스의 설정 정보를 정의하기 위해 사용하는 파일이다.

Prisma를 가장 처음 초기화 하였을 때, prisma.schema 파일을 확인한다면, 아래의 2가지 구문이 작성되어 있는 것을 확인할 수 있다.

 ● datasource

  • 데이터베이스에 대한 정의를 하기 위해 사용된다.
  • Prisma가 어떤 데이터베이스 엔진을 사용할 것인지, 데이터베이스의 위치 ( URL )는 어디인지 등의 정보를 정의하는데 사용된다.

 ● generator

  • Prisma 클라이언트를 생성하는 방식을 설정하는 구문이다.

 

Prisma datasource

Prisma가 데이터베이스를 연결할 수 있도록 설정하고, 관리하는 데 필요한 정보를 설정하는 구문이다.

우선 Prisma는 연결하려는 데이터베이스의 속성을 schema.prisma 파일에서 관리하고 있다.

여기서 datasource 프로퍼티에 정의한 속성들을 수정해 사용자 아이디, 비밀번호, 엔드 포인트 등 다양한 설정값을 입력해주어야 한다.

 

datasource 설정

// schema.prisma

datasource db {
  // MySQL 데이터베이스 엔진을 사용합니다.
  provider = "mysql"
  // 데이터베이스 연결 정보를 .env 파일의 DATABASE_URL 로부터 읽어옵니다.
  url      = env("DATABASE_URL")
}

 

처음 생성된 datasource 구문은 위와 같이 작성되어 있다. 각 프로퍼티들은 아래와 같은 속성을 가지고 있다.

 1. provider : Prisma 가 사용할 데이터베이스 엔진의 유형

 2. url : 데이터베이스를 연결하기 위한 URL

 

url 부분에서 env("DATABASE_URL") 방식으로, 데이터베이스의 주소가 노출되지 않게 작성하는 dotenv 의 문법을 사용하고 있다. env() 문법은 프로젝트의 root 폴더에 있는 .env 파일에 정의되어 있는 정보를 해당 schema.prisma 파일로 불러오는 것이다.

여기서, dotenv 는 어플리케이션의 환경 변수를 관리하는 모듈이다. 실제 코드에서 민감한 정보를 노출시키지 않도록 보호해주고, 개발 환경에 따라 다르게 설정해야 하는 값을 별도의 파일에서 관리할 수 있게 해준다.

 

env 파일 살펴보기

# .env
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

 

Prisma를 초기화 하고, .env 파일을 확인하면, 위와 같은 내용을 확인할 수 있다.

.env 파일은 key-value의 형태로 구성되어 있고, DATABASE_URL 이라는 하나의 변수가 선언되어 있다.

이곳의 URL을 변경하게 되면 해당 데이터베이스와 연결이 가능하다.

 

데이터베이스 URL

데이터베이스 URL은 Prisma가 어떤 데이터베이스와 어떻게 연결할지를 알려주는 중요한 정보다. 

URL 내부에는 데이터베이스 엔진 유형, 사용자 아이디, 패스워드와 같은 정보가 포함된다.

.env 파일의 DATABASE_URL에서 확인한 것처럼, 데이터베이스와 연결하기 위해선 URL을 생성해야 한다.

DATABASE_URL은 어떻게 구성되어 있는지, 그리고 어떻게 구성하는지를 확인해보도록 하자.

 

AWS RDS를 대여해 받아온 RDS의 엔드 포인트, 사용자 아이디, 비밀번호, Port 번호를 바탕으로 Prisma와 연결하기 위한 URL을 작성해보도록 하자.

Database URL

 

데이터베이스 URL은 크게 4가지로 나뉘어진다.

 ● Protocol

  • Prisma가 사용할 데이터베이스 엔진을 나타낸다.
  • postgresql, sqllite, mysql과 같은 데이터베이스 엔진을 정의한다.

 ● Base URL

  • 데이터베이스의 엔드 포인트와 아이디, 패스워드, 포트 번호를 나타낸다.
  • <Id>:<Password>@<RDS Endpoint>:<Port> 의 형태로 구성된다.

 ● Path

  • MySQL에서 사용할 데이터베이스 이름을 설정하는 구성 요소다.

 ● Arguments

  • Prisma에서 데이터베이스 연결을 설정하는데 필요한 추가 옵션을 나타낸다.
  • 데이터베이스와 연결할 수 있는 최대 커넥션 갯수, 타임아웃 시간 등이 있다.

 

Prisma의 데이터베이스 URL 구현해보기

 

AWS RDS에 접속하기 위한 URL의 속성값은 아래와 같다.

  • 데이터베이스 엔진 : mysql
  • 마스터 사용자 이름 : root
  • 마스터 암호 : aaaa4321
  • RDS 엔드포인트 : express-database.clxfpepff75.ap-northeast-2.rds.zmazonaws.com
  • Port 번호 : 3306
  • 사용할 DB 이름 : prisma_crud
mysql://root:aaaa4321@express-database.clxfpepff75.ap-northeast-2.rds.zmazonaws.com:3306/prisma_crud

 

.env 파일의 DATABASE_URL을 위 작성한 내용을 입력해 변경한다.

# .env

DATABASE_URL="mysql://root:aaaa4321@express-database.clxfpepff75.ap-northeast-2.rds.zmazonaws.com:3306/prisma_crud"

 


Prisma model

Prisma의 model 구문은 특정 Table과 Column의 속성값을 입력해, 데이터베이스와 Express 프로젝트를 연결 시켜준다.

  • model 구문은 Prisma를 사용할 때 가장 많이 작성하게 될 구문이며, Prisma가 사용할 데이터베이스의 테이블 구조를 정의하기 위해 사용된다.
  • schema.prisma 파일에서 model에 작성된 정보를 바탕으로 Prisma Client를 통해 Javascript에서 MySQL의 테이블을 조작할 수 있게 된다.
  • model 구문은 Javascript에서 MySQL의 테이블을 사용하기 위한 다리 역할을 수행하며, MySQL과 실제 연결되어 사용할 수 있게 도와준다.

 

그렇다면, 상품 ( Products )을 담당하는 테이블을 어떻게 구현하는지 확인해보자.

Products 테이블 예시

// schema.prisma

model Products {
  productId   Int     @id @default(autoincrement()) @map("productId")
  productName String  @unique @map("productName")
  price       Int     @default(1000) @map("price")
  info        String? @map("info") @db.Text

  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  @@map("Products")
}

 

  • 데이터 유형은 각 필드의 데이터를 어떤 형식으로 저장할 것인지 결정하게 된다.
  • Prisma에서 다양한 데이터 유형을 지원하는데, 위 예시에서 Int, String, DateTime 등의 데이터 유형이 사용된다.
  • 데이터 유형 뒤에 ? 가 붙으면, NULL을 허용하는 컬럼이 된다.
  • SQL에서 사용하는 것과 동일하게, UNIQUE 제약 조건과 AUTO_INCREMENT 제약조건을 사용할 수 있다.
  • 왼쪽에 있는 이름은 Node.js에서 해당 Prisma를 사용할 때 쓰는 이름이고, 오른쪽에 있는 @map("")는 데이터베이스 상에 기록될 이름을 말한다.
  • String이면 기본적으로는 VARCHAR을 의미하는데 @db.Text를 붙이면 Text 타입을 가진다.
  • updatedAt은 자동으로 업데이트 된 시간을 저장한다. 
  • @@map("Products")는 Products 테이블을 MySQL에서 Products 란 이름으로 사용한다는 의미다. ( @@map() 을 작성하지 않으면, 테이블명의 대문자는 전부 소문자로 치환된다 )

Products 테이블의 요구사항

Name 타입 ( Type ) NULL 제약 조건 default
productId ( PK ) INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT
productName STRING NOT NULL UNIQUE  
price INTEGER NOT NULL   1000
info TEXT NULL    
createdAt DATETIME NOT NULL   현재 시간
updatedAt DATETIME NOT NULL   현재 시간

 

Products 테이블의 생성 .sql 파일

-- CreateTable
CREATE TABLE `Products` (
    `productId` INTEGER NOT NULL AUTO_INCREMENT,
    `productName` VARCHAR(191) NOT NULL,
    `price` INTEGER NOT NULL DEFAULT 1000,
    `info` TEXT NULL,
    `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    `updatedAt` DATETIME(3) NOT NULL,

    UNIQUE INDEX `Products_productName_key`(`productName`),
    PRIMARY KEY (`productId`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  • string 타입은 VARCHAR(191) 의 형식을 가진다.
  • DateTime 타입은 DATETIME(3) 의 형식을 가진다.
  • Text? 타입은 Text 타입과 함께, NULL 제약 조건을 가진다.

 

Posts 테이블 요구사항

Name 타입 ( Type ) NULL 제약 조건 default
postId ( PK ) INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT
title STRING NOT NULL    
content TEXT NOT NULL    
password STRING NOT NULL    
createdAt DATETIME NOT NULL   현재 시간
updateAt DATETIME NOT NULL   현재 시간

 

// schema.prisma

model Posts {
  postId    Int      @id @default(autoincrement()) @map("postId")
  title     String   @map("title")
  content   String   @map("content") @db.Text
  password  String   @map("password")
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  @@map("Posts")
}

 

 

Prisma DB, Table 생성하기

# schema.prisma 파일에 설정된 모델을 바탕으로 MySQL에 정보를 업로드합니다.
npx prisma db push

 

위 Prisma CLI 명령어를 이용해 schema.prisma 파일에 정의한 내용을 바탕으로 새로운 DB와 테이블을 생성한다.

생성된 모습


Prisma CLI 더 알아보기

 ● prisma db push

  • schema.prisma 파일에 정의한 설정값을 실제 데이터베이스에 반영한다.
  • 내부적으로 prisma generate가 실행된다.
  • 데이터베이스 구조를 변경하거나 새로운 테이블을 생성할 수 있다.

 ● prisma_init

  • Prisma를 사용하기 위한 초기 설정을 생성한다.
  • 이 명령어를 실행하면 schema.prisma 파일과 같은 필요한 설정 파일들이 생성된다.

 ● prisma generate

  • Prisma Client를 생성하거나 업데이트 한다.
  • 대표적으로, schema.prisma 파일에 변경 사항이 생겼거나, 데이터베이스 구조가 변경되었을 때, 이 명령어를 사용해 Prisma Client를 최신 상태로 유지할 수 있다.

 ● prisma db pull

  • 현재 연결된 데이터베이스의 구조를 prisma.schema 파일로 가져온다. ( pull )
  • 데이터베이스에서 구조 변경이 발생했을 때, 이 명령어를 사용하면 Prisma Schema를 최신 상태로 유지할 수 있다.
  • 이후 prisma generate 명령어를 사용해 변경 사항을 Prisma Client에 반영할 수 있다.

더욱 다양한 Prisma CLI 명령어 https://www.prisma.io/docs/reference/api-reference/command-reference#synopsis

 

Prisma CLI reference | Prisma Documentation

This page gives an overview of all available Prisma CLI commands, explains their options and shows numerous usage examples.

www.prisma.io


Prisma Client

 

Prisma는 model을 generate 하면, 해당 모델에 대한 정보가 node_modules 폴더 내에 있는 Prisma Client에 전달된다.

( prisma db push도 내부적으로 generate 가 실행된다. )

Prisma Client는 Prisma Schema에 정의한 데이터베이스 모델 ( model )을 TypeScript 코드로 변환해, 개발자가 데이터베이스와 상호작용할 수 있게 해준다. 이러한 과정을 통해, 데이터베이스를 JavaScript에서 손쉽게 다룰 수 있게 되고, Prisma Schema와 동기화된 Prisma Client를 이용해 데이터베이스를 사용할 수 있게 된다.

 

Prisma Client 확인해보기

// node_modules/.prisma/client/index.d.ts

export type ProductsPayload<ExtArgs extends $Extensions.Args = $Extensions.DefaultArgs> = {
  name: "Products"
  objects: {}
  scalars: $Extensions.GetResult<{
    productId: number
    productName: string
    price: number
    info: string | null
    createdAt: Date
    updatedAt: Date
  }, ExtArgs["result"]["products"]>
  composites: {}
}

/**
 * Model Products
 * 
 */
export type Products = runtime.Types.DefaultSelection<ProductsPayload>

 

  • schema.prisma 파일에 정의한 내용처럼, Products 테이블에 대한 내용이 위와 같이 작성되어 있다.
  • schema.prisma 정의한 내용을 prisma generate를 이용해 index.d.ts에 추가하고 최종적으로는 index.d.ts에 있는 내용을 바탕으로 prisma를 구성한다.

 

Prisma Method

Prisma는 mongoose와 동일하게, findMany(), findFirst(), findUnique() 등 다양한 메서드를 지원한다.

mongoose를 사용했을 때는 Schema를 이용해 DB를 사용했다면, Prisma에서는 Prisma Client를 이용해 MySQL의 데이터를 조작한다.

 

Posts 테이블의 구조를 살펴보자.

Posts 테이블

 

Posts 테이블은 게시글 제목 ( title ), 내용 ( content ), 비밀번호 ( password ) 총 3개의 컬럼을 가지고 있고 postId, createdAt, updatedAt 컬럼은 아무런 데이터를 입력하지 않더라도 기본값을 가질 수 있도록 구성되어 있다.

 

그러면 게시글을 생성 및 수정할 때 필수 인자값 3개를 이용해 권한 검증 및 데이터 생성을 구현해보자.

 

API를 구현하기 앞서 routes/posts.router.js 파일을 생성하고 express 프로젝트를 초기화 하자.

// routes/posts.router.js

import express from 'express';
import { PrismaClient } from '@prisma/client';

const router = express.Router(); // express.Router()를 이용해 라우터를 생성한다.
const prisma = new PrismaClient({
  // Prisma를 이용해 데이터베이스를 접근할 때, SQL을 출력해준다.
  log: ['query', 'info', 'warn', 'error'],

  // 에러 메시지를 평문이 아닌, 개발자가 읽기 쉬운 형태로 출력해준다.
  errorFormat: 'pretty',
}); // PrismaClient 인스턴스를 생성한다.

export default router;

 

// app.js

import express from 'express';
import PostsRouter from './routes/posts.router.js';

const app = express();
const PORT = 3017;

app.use(express.json());
app.use('/api', [PostsRouter]);

app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});


Prisma 게시글 생성 ( Create ) API

게시글 생성 API의 비즈니스 로직

  1. title, content, password 를 body로 전달받는다.
  2. title, content, password 를 이용해 Posts 테이블에 데이터를 삽입 한다.
  3. 생성된 게시글을 반환한다.

Prisma 게시글 생성 API

// routes/posts.router.js

// 게시글 생성
router.post('/posts', async (req, res, next) => {
  const { title, content, password } = req.body;
  const post = await prisma.posts.create({
    data: {
      title,
      content,
      password,
    },
  });

  return res.status(201).json({ data: post });
});

 

create 메서드를 이용해 데이터를 생성한다.

 

Prisma 게시글 조회 ( Read ) API

게시글 조회 API는 게시글 목록 조회, 게시글 상세 조회 2개의 API로 구현할 수 있다.

게시글 목록 조회 API의 경우 게시글의 내용 ( content )을 제외하고,

게시글 상세 조회 API의 경우에만 게시글의 전체 내용이 출력되도록 만들어 볼 예정

 

Prisma 게시글 목록 조회 API

// routes/posts.router.js

/** 게시글 전체 조회 API **/
router.get('/posts', async (req, res, next) => {
  const posts = await prisma.posts.findMany({
    select: {
      postId: true,
      title: true,
      createdAt: true,
      updatedAt: true,
    },
  });

  return res.status(200).json({ data: posts });
});

 

 

Prisma 게시글 상세 조회 API

// routes/posts.router.js

/** 게시글 상세 조회 API **/
router.get('/posts/:postId', async (req, res, next) => {
  const { postId } = req.params;
  const post = await prisma.posts.findFirst({
    where: { postId: +postId },
    select: {
      postId: true,
      title: true,
      content: true,
      createdAt: true,
      updatedAt: true,
    },
  });

  return res.status(200).json({ data: post });
});

 

게시글 목록 조회 API는 findMany() 메서드를 이용해 Posts 테이블이 가지고 있는 모든 데이터들을 배열의 형태로 조회한다.

게시글 상세 조회 API는 특정한 게시글 1개만 출력해야 하니까 findFirst() 메서드를 이용해 Posts 테이블에 특정한 데이터 1개를 조회한다.

 

select의 역할

Prisma는 schema.prisma model에 설정한 정보를 바탕으로 해당하는 SQL을 생성한다.

게시글 목록 조회 API의 응답 ( response )은 postId, title, createdAt, updatedAt 4가지의 컬럼만 출력되어야 한다.

만약 select 속성을 사용하지 않을 경우 전체 목록에서 출력되지 않아야하는 password, content 컬럼이 출력되게 된다.

 

postId

postId는 변수 명 앞에, + 연산자가 붙은 경우, 문자열 타입을 숫자형 타입으로 변환해준다.

postId를 타입 변환하지 않은 상태로 데이터를 조회하려 하면 아래와 같은 에러가 발생할 수 있다.

Argument Error

 

위 에러는 숫자형 컬럼을 문자열 데이터로 조회했기 때문에 발생하는 에러다.

Posts 테이블을 schema.prisma model에서 정의할 때, postId 컬럼을 INTEGER로 선언했다. 

따라서 해당하는 정ㅈ보를 조회할 때에도 숫자형 타입으로 조회를 해야하는 것.

 

postId와 같은 방법 이 외에도 아래와 같은 방법으로 타입 변환을 사용할 수 있다.

where: { postId: parseInt(postId) },

다만, 이 방법은 변환 함수가 명시적인 점이 장점이긴 하지만, 표현하기에 불편하다는 문제점이 있다.

+ 연산자를 사용하면 이를 좀 더 간결하게 표현할 수 있다.

 

Prisma의 조회 메서드 좀더 자세한 정보 : https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#model-queries

 

Prisma Client API | Prisma Documentation

API reference documentation for Prisma Client.

www.prisma.io

 

Prisma 게시글 수정 ( Update ) API

게시글 수정 API의 비즈니스 로직

  1. Path Parameters로 어떤 게시글을 수정할 지 postId를 전달받는다.
  2. 변경할 title, content와 권한 검증을 위한 password를 body로 전달받는다.
  3. postId를 기준으로 게시글을 검색하고, 게시글이 존재하는지 확인한다.
  4. 게시글이 조회되었다면 해당하는 게시글의 password가 일치하는지 확인한다.
  5. 모든 조건을 통과했으면 게시글을 수정한다.
// routes/posts.router.js

/** 게시글 수정 API **/
router.put('/posts/:postId', async (req, res, next) => {
  const { postId } = req.params;
  const { title, content, password } = req.body;

  const post = await prisma.posts.findUnique({
    where: { postId: +postId },
  });

  if (!post)
    return res.status(404).json({ message: '게시글이 존재하지 않습니다.' });
  else if (post.password !== password)
    return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });

  await prisma.posts.update({
    data: { title, content },
    where: {
      postId: +postId,
      password,
    },
  });

  return res.status(200).json({ data: '게시글이 수정되었습니다.' });
});

 

게시글 수정 API의 경우 모든 비즈니스 로직을 수행한 이후 데이터를 수정하도록 구현했다.

게시글을 수정하는 update 메서드에서 특정한 게시글을 바로 수정하는 것이 아니라, 한번 더 postId, password를 검증해 안전하게 게시글을 수정하는 것을 확인할 수 있다.

 

where 속성의 조건

SQL에서는 특정한 데이터를 검출하기 위해 where절에서 OR, AND, LIKE, 정규표현식 등 다양한 연산자를 사용할 수 있다.

Prisma의 where절은 여러개의 조건이 들어올 경우 AND 연산자를 사용한 것과 동일한 결과를 출력해준다.

이외의 연산자를 사용하고 싶으면, 아래와 같은 문법으로도 사용할 수 있다.

await prisma.users.findMany({
  where: {
    OR: [
      {
        email: {
          endsWith: 'prisma.io',
        },
      },
      { email: { endsWith: 'gmail.com' } },
    ],
    NOT: {
      email: {
        endsWith: 'hotmail.com',
      },
    },
  },
})

 

Prisma의 논리 연산자에 대한 자세한 정보 : https://www.prisma.io/docs/concepts/components/prisma-client/filtering-and-sorting#combining-operators

 

Filtering and Sorting (Concepts) | Prisma Documentation

Use Prisma Client API to filter records by any combination of fields or related record fields, and/or sort query results.

www.prisma.io

 

Prisma 게시글 삭제 ( Delete ) API

게시글 삭제 API는 게시글 수정 API와 동일한 로직을 수행하지만 Body에서 password만 전달받는 것이 유일한 차이다.

 

게시글 삭제 API의 비즈니스 로직

  1. Path Parameters로 어떤 게시글을 수정할 지 postId를 전달받는다.
  2. 권한 검증을 위한 password를 body로 전달받는다.
  3. postId를 기준으로 게시글을 검색하고, 게시글이 존재하는지 확인한다.
  4. 게시글이 조회되었다면 해당하는 게시글의 password가 일치하는지 확인한다.
  5. 모든 조건을 통과하면 게시글을 삭제한다.
// routes/posts.router.js

/** 게시글 삭제 API **/
router.delete('/posts/:postId', async (req, res, next) => {
  const { postId } = req.params;
  const { password } = req.body;

  const post = await prisma.posts.findFirst({ where: { postId: +postId } });

  if (!post)
    return res.status(404).json({ message: '게시글이 존재하지 않습니다.' });
  else if (post.password !== password)
    return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });

  await prisma.posts.delete({ where: { postId: +postId } });

  return res.status(200).json({ data: '게시글이 삭제되었습니다.' });
});

 


 

Prisma 리팩토링

PrismaClient 는 Prisma를 사용해 실제 데이터베이스와의 연결을 관리하는 객체다.

new PrismaClient() 를 이용해 Javascript에서 Prisma를 사용할 수 있도록 인스턴스를 생성하게 된다.

const prisma = new PrismaClient();

 

앞서 작성한 게시글 ( Posts ) 라우터만 구현했지만, 이후에 사용자 ( Users ), 사용자 정보 ( UserInfos ), 해시 태그 ( HashTags )와 같은 여러 라우터들이 추가된다면, 각각의 라우터 갯수마다 데이터베이스와 연결하게 되는 문제가 발생한다.

여러번 데이터베이스의 연결을 생성한다면, 리소스가 과도하게 사용되고, 그로인해 어플리케이션의 성능이 저하될 수 있다. 따라서, 최대한 데이터베이스의 연결을 줄이는 것이 효율적인 방법이다.

이런 문제를 해결하기 위해 /utils/prisma/index.js 파일을 구현해, 하나의 파일에서 데이터베이스 커넥션을 관리해 최초로 1번만 MySQL과 커넥션을 생성하도록 코드를 구현하면 된다.

 

utils/prisma/index.js Prisma 리팩토링

// utils/prisma/index.js

import { PrismaClient } from '@prisma/client';

export const prisma = new PrismaClient({
  // Prisma를 이용해 데이터베이스를 접근할 때, SQL을 출력해줍니다.
  log: ['query', 'info', 'warn', 'error'],

  // 에러 메시지를 평문이 아닌, 개발자가 읽기 쉬운 형태로 출력해줍니다.
  errorFormat: 'pretty',
}); // PrismaClient 인스턴스를 생성합니다.

 

'데이터베이스 > 실습' 카테고리의 다른 글

[DATABASE][실습] Raw Query 실습  (0) 2024.09.06

Prisma는 ORM ( Object Relational Mapping )으로써 Javascript 객체 ( Object )와 데이터베이스의 관계 ( Relation )을 연결 해주는 도구다.

 

Prisma와 같은 ORM은 여러가지의 관계형 데이터베이스 ( RDB )를 사용할 수 있다.

 

Prisma vs mongoose

  • mongoose의 경우 ODM ( Object Document Mapping ) 으로 Javascript의 객체를 Document와 연결한다.
  • Prisma의 경우 앞서 언급했듯이 ORM ( Object Relational Mapping ) 으로 Javascript의 객체와 데이터베이스의 관계 ( Relation )를 연결해주는 차이점이 있다.
  • mongoose를 지원하는 데이터베이스는 MongoDB 밖에 존재하지 않지만, Prisma의 경우 RDBMS에 해당하는 다양한 데이터베이스를 사용할 수 있다는 장점이 있다. ( 미약하지만, Prisma의 경우 MongoDB를 지원함 )
  • mongoose의 경우 Schema의 형태로 컬렉션 ( Collection )에 대한 속성을 설정한다면, Prisma의 경우 Model의 형태로 테이블 ( Table )의 속성을 설정할 수 있다.

 

ORM의 장단점

  Prisma와 같은 ORM을 사용하는 가장 큰 이유는 대표적으로 2가지가 있다.

  1. 프로덕션에서 사용하는 데이터베이스가 언제바뀔 지 알 수 없다.
  2. 데이터베이스에서 사용하는 DB 또는 Table 속성이 변경되었을 때 빠르게 수정이 가능하다.

이러한 장점을 가지고 있어도 ORM은 만능이 아니다.

JOIN 과 UNION 연산자를 동시에 사용하는 복잡한 쿼리를 작성할 경우, ORM으로 구현하기 위해 SQL 보다는 ORM을 더 깊게 이해해야 하는 상황이 발생할 수 있다. 

또한 앞서 말한 복잡한 쿼리를 작성하거나 ( 서브 쿼리 포함 ) ORM의 SQL로 변환해주는 시간 조차 아까운 극한의 성능을 요구하는 쿼리가 필요한 상황에서는 Raw Query를 사용하는 것이 더욱 좋을 수 있다.

 

 

Node.js 에서 Raw Query를 사용하기 위해서 AWS RDS에서 대여받은 MySQL에 연결을 도와주는 데이터베이스 드라이버가 필요하다. 데이터베이스에 직접 SQL을 요청하고, 테이블을 생성하거나, 데이터를 삽입하는 API를 mysql2 라이브러리를 이용해 구현해보자.

 

Raw Query 라이브러리 설치

# yarn으로 프로젝트를 초기화합니다.
yarn init -y

# express와 mysql 드라이버를 설치합니다.
yarn add express mysql2

 

mysql2는 MySQL 데이터베이스를 Node.js에서 사용할 수 있게 도와주는 라이브러리다.

데이터베이스와 개발 언어 사이를 연결해주는 역할을 담당하기 때문에, 데이터베이스 드라이버라는 이름으로도 불린다.

 

데이터베이스 연결하기

우선, app.js 파일을 만들고, mysql2 라이브러리를 사용해 AWS RDS의 MySQL과 연결을 설정해야 한다.

 

// app.js
import express from 'express';
import mysql from 'mysql2';

const connect = mysql.createConnection({
  host: 'AWS RDS 엔드포인트', // AWS RDS 엔드포인트
  user: 'root', // AWS RDS 계정 명
  password: 'aaaa4321', // AWS RDS 비밀번호
  database: 'express_db', // 연결할 MySQL DB 이름
})

const app = express();
const PORT = 3017;

app.use(express.json());

app.listen(PORT, () => {
  console.log(PORT, '포트로 서버 열림!');
});

 

mysql2 라이브러리를 읽어오고, createConnection() 함수를 이용해 MySQL DB와 연결한다.

 

mysql2 데이터베이스 연결 속성 알아보기

  • host ( mysql2 데이터베이스 드라이버가 접속할 데이터베이스의 주소를 나타낸다. )
  • user ( 데이터베이스의 계정 명을 나타낸다. )
  • password ( 데이터베이스의 비밀번호를 나타낸다. )
  • database ( 데이터베이스의 이름을 나타낸다. )

이 외에도, timezone 으로 시간대를 설정하거나, ssl로 SSL 인증서를 설정하는 등 다양한 옵션을 설정할 수 있다.

 

테이블 생성 API 

CREATE TABLE sql로 테이블을 생성할 수 있다. 

클라이언트로부터 생성할 테이블 이름을 tableName으로 전달받아서 새로운 테이블을 생성해보자.

 

테이블 생성 API 테이블 구조

Name 타입 NULL 제약 조건 default
id ( PK ) INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT
name STRING NOT NULL    
createdAt DATETIME NOT NULL   현재 시간

 

/** 테이블 생성 API **/
app.post('/api/tables/', async (req, res, next) => {
  const { tableName } = req.body;

  await connect.promise().query(`
      CREATE TABLE ${tableName}
      (
          id        INT         NOT NULL AUTO_INCREMENT PRIMARY KEY,
          name      VARCHAR(20) NOT NULL,
          createdAt DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP
      )`);

  return res.status(201).json({ message: '테이블 생성에 성공하였습니다.' });
});

 

Raw Query의 사용법

  • mysql2 라이브러리에서 Raw Query는 connect.promise().query() 형식으로 사용한다.

 

테이블 목록 조회 API 

 

SHOW TABLES sql로 테이블을 조회할 수 있다.

/** 테이블 조회 API **/
app.get('/api/tables', async (req, res, next) => {
  const [tableList] = await connect.promise().query('SHOW TABLES');
  const tableNames = tableList.map(table => Object.values(table)[0]);

  return res.status(200).json({ tableList: tableNames });
});

 

Raw Query의 결과값을 const [tableList] 의 형태로 할당하는 이유

  • Raw Query를 사용할 때, CREATE TABLE 명령어와 같이 데이터를 생성하는 명령어의 경우 반환하는 값이 존재하지 않았지만, SHOW TABLES 또는 SELECT 문법의 조회 명령어는 반환값이 존재한다.
  • mysql2의 경우 Raw Query를 이용해 조회된 결과값은 배열의 첫번째에 할당되게 되는데, 그렇기 때문에 배열 구조 분해 할당 문법을 이용해 배열의 첫번째의 값만 tableList 변수에 할당해 사용한다.

 

데이터 삽입 API

 

INSERT INTO sql로 데이터를 삽입할 수 있다.

 

클라이언트로부터 Params로 전달받은 tableName에 해당하는 테이블에 Body 데이터인 name을 삽입하도록 해보자.

/** 데이터 삽입 API **/
app.post('/api/tables/:tableName/items', async (req, res, next) => {
  const { tableName } = req.params;
  const { name } = req.body;

  await connect.promise().query(`
      INSERT INTO ${tableName} (name)
      VALUES ('${name}')`);
  return res.status(201).json({ message: '데이터 생성에 성공하였습니다.' });
});

 

데이터 조회 API

SELECT sql로 데이터를 조회할 수 있다.

 

클라이언트로부터 Params로 전달받은 tableName에 해당하는 테이블의 모든 데이터를 조회해보자.

/** 데이터 조회 API **/
app.get('/api/tables/:tableName/items', async (req, res, next) => {
  const { tableName } = req.params;

  const [itemList] = await connect.promise().query(`
      SELECT id, name, createdAt
      FROM ${tableName}`);

  return res.status(200).json({ itemList: itemList });
});

 

 

'데이터베이스 > 실습' 카테고리의 다른 글

[DATABASE][실습] Prisma 시작하기  (0) 2024.09.06

Raw Query는 SQL을 Node.js에서 사용해 데이터베이스에 쿼리를 요청 할 수 있는 방법이다.

SQL을 알고 있으면 다양한 데이터베이스에 연결해 테이블을 생성하거나 데이터를 조회하는 등 다양하게 

데이터베이스와 상호작용을 할 수 있다.

 

더불어, 긴 쿼리를 수행하거나 트랜잭션을 직접적으로 관리하는 등 데이터베이스가 지원하는 대다수의 기능을

SQL만으로 간편하게 사용할 수 있는 장점을 가지고 있다.

 

 

'데이터베이스' 카테고리의 다른 글

[DATABASE] 트랜잭션 ( Transaction )  (1) 2024.09.11
[DATABASE] ORM과 Prisma  (0) 2024.09.06
[DATABASE] SELECT JOIN 연산자  (0) 2024.09.05
[DATABASE] SQL 제약조건  (0) 2024.09.05
[DATABASE] SQL ( Structured Query Language )  (0) 2024.09.05

JOIN 연산자는 두 테이블 사이의 공통된 데이터를 기준으로 테이블을 연결해 하나의 테이블처럼 조회 할 수 있게 해주는 연산자다.

 

여러 테이블 간의 외래 키 ( Foreign Key )로 설정된 컬럼들을 연결해 조회하는 SELECT 연산자의 활용법 중 하나다.

JOIN 연산자는 데이터를 조회할 때, 여러 테이블들의 연관 관계를 포함해 데이터를 조회한다.

 

 

위와 같은 ERD는 사용자 ( Users ) 테이블과 게시글 ( Posts ) 테이블에서 1명의 사용자가 여러개의 게시글을 작성 할 수 있으므로 1:N 관계를 갖게 된다.

여기서 게시글 목록을 조회할 때 작성한 사용자의 이메일을 표시하고 싶어도 게시글 테이블에 email 컬럼이 존재하지 않아 이메일을 표시해줄 수 없게 된다.

이때, 게시글 테이블에서는 userId 컬럼을 이용해 어떤 사용자가 게시글을 작성했는지 확인 할 수 있으므로, JOIN을 이용해 외래키가 설정된 userId를 기준으로 해당 사용자의 이메일을 함께 출력해 줄 수 있다.

 

예시를 통해 확인해보자.

 사용자, 게시글 생성 및 삽입 SQL

CREATE TABLE Users
(
    userId   int(11)      NOT NULL AUTO_INCREMENT PRIMARY KEY,
    email    varchar(255) NOT NULL,
    password varchar(255) NOT NULL
);

CREATE TABLE Posts
(
    postId  int(11)      NOT NULL AUTO_INCREMENT PRIMARY KEY,
    userId  int(11)      NOT NULL,
    title   varchar(255) NOT NULL,
    content varchar(255) NOT NULL,
    FOREIGN KEY (userId) REFERENCES Users (userId)
);


INSERT INTO Users (userId, email, password)
VALUES (1, 'AAAA', '1234'),
       (2, 'BBBB', '1234');

INSERT INTO Posts (userId, title, content)
VALUES (1, 'AAAA Title1', 'content'),
       (1, 'AAAA Title2', 'content'),
       (2, 'BBBB Title1', 'content'),
       (2, 'BBBB Title2', 'content');

 

사용자, 게시글 테이블 JOIN SQL

SELECT p.postId, p.title, p.content, u.email
FROM Posts as p
JOIN Users as u
	ON p.userId = u.userId;

 

SELECT JOIN 결과

 

JOIN ... ON 의 역할

JOIN ... ON 은 두 개 이상의 테이블을 결합할 때, 어떤 조건으로 테이블을 결합할 것인지를 정의할 때 사용한다.

  • ON 뒤에는 결합 조건이 위치하며, 이는 두 테이블 간의 관계를 정의한다.

이번 예시는 사용자( Users ) 테이블에 있는 userId가 게시글 ( Posts ) 테이블에 있는 userId로 외래 키가 설정되어 있었고, 해당 컬럼을 기준으로 게시글의 작성자를 확인할 수 있었다.

 

'데이터베이스' 카테고리의 다른 글

[DATABASE] ORM과 Prisma  (0) 2024.09.06
[DATABASE] Raw Query  (0) 2024.09.06
[DATABASE] SQL 제약조건  (0) 2024.09.05
[DATABASE] SQL ( Structured Query Language )  (0) 2024.09.05
[DATABASE] 관계형 데이터베이스  (0) 2024.09.05

제약 조건 ( Constraint )은 각 컬럼들간의 제안사항을 관리하고,

조건을 위반하는 데이터를 방지해 데이터베이스의 무결성 ( Integrity )을 보장하는 규칙이다.

 

무결성은 데이터가 결함없이 정확하고 완전한 상태를 의미한다.

 

제약 조건의 종류

 ● 고유 제약 조건 ( Unique )

  • 테이블에 소속된 특정 컬럼이 중복된 키를 가질 수 없는 조건
  • 사용자 아이디, 이메일과 같은 고유한 정보를 저장할 때 사용한다.

 ●  NULL 제약 조건 ( Null )

  • 특정 컬럼이 아무런 값을 입력받지 않도록 설정하거나 무조건 값을 입력 받도록 설정하는 조건
  • 데이터가 없으면 Null을 저장해 데이터가 존재하지 않다는것을 표현한다.

 ● 기본 키 제약 조건 ( Primary Key )

  • 테이블 내에서 각 행 ( row )을 고유하게 식별할 수 있도록 보장하는 조건

 ● 외래 키 제약 조건 ( Foreign Key )

  • 데이터베이스를 설계할 때 가장 많은 고민을 하게 될 테이블 간의 관계를 설정하는 조건
  • 한 테이블의 컬럼 ( Column )이 다른 테이블의 특정 행 ( Row )을 참조하도록 설정하는 조건

음식 주문앱 DB 설계

  • 고객 ( User ) 테이블

  • 음식 ( Food ) 테이블

 

연관 관계 설계

  • 고객이 음식 주문 시, 주문 정보를 어느 테이블에 넣어야 할까?
  • 고객 테이블 또는 음식 테이블

"고객 테이블"에 주문 정보를 넣어보자

  • 문제점 : 회원 중복

"음식 테이블"에 주문 정보를 넣어보자

  • 그럴듯 해 보이지만.. 먹깨비 ( userID = 2 )가 '후라이드치킨'을 하나 더 시킬 경우.. 문제가 생김
  • 문제점 : 음식 중복

'주문'을 위한 테이블이 필요 ▶ 주문 ( Order ) 테이블 추가

  • 회원 1명은 주문 N개를 할 수 있다. ( 회원 : 주문 = 1 : N 관계 )
  • 음식 1개는 주문 N개에 포함될 수 있다. ( 음식 : 주문 = 1 : N 관계 )
  • 결론적으로 ( 회원 : 음식 = N : M 관계 )

ERD ( 개체 관계도 )

음식 주문앱에 사용할 테이블 ERD

  • SQL 문
CREATE TABLE User
(
    userId int(11)      NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name   varchar(255) NOT NULL UNIQUE
);

CREATE TABLE Food
(
    foodId int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name   varchar(255),
    price  int(11)
);

CREATE TABLE Order
(
    orderId   int(11)  NOT NULL AUTO_INCREMENT PRIMARY KEY,
    userId    int(11)  NOT NULL,
    foodId    int(11)  NOT NULL,
    createdAt datetime NOT NULL DEFAULT NOW(),
    FOREIGN KEY (foodId) REFERENCES Food (foodId)
        ON DELETE NO ACTION
        ON UPDATE CASCADE,
    FOREIGN KEY (userId) REFERENCES User (userId)
        ON DELETE NO ACTION
        ON UPDATE CASCADE
);

 


 

기본 키 ( Primary Key ) 제약 조건

위 CREATE TABLE SQL문을 보면 PRIMARY KEY라는 조건으로 기본 키가 선언되어 있는 것을 확인 할 수 있다.

만약, 기본키를 설정하지 않은 테이블을 생성하려한다면, 특정 데이터를 찾기위한 고유한 정보가 존재하지 않기 때문에 아래와 같은 에러메세지가 출력되면서 테이블이 생성되지 않게 된다.

ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key

 

CREATE TABLE Users
(
    userId int(11)      NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name   varchar(255)
);

 

사용자 ( User ) 테이블

  • userId 컬럼을 기본키로 설정한다. ( AUTO_INCREMENT 속성은 데이터가 입력될때마다 해당 속성의 값이 1씩 자동으로 증가시켜주는 속성 )
  • 이름 ( name ) 컬럼을 가진다.

 

NULL 제약 조건

테이블에서 컬럼을 생성할 때 추가 조건을 작성하지 않으면 기본적으로 해당 컬럼은 NULL값을 허용하는 상태가 된다.

NULL 값을 허용하게 되면, 필수적으로 입력되어야 하는 데이터가 누락될 수 있다는 점을 기억하고 있어야한다.

 

NOT NULL 제약 조건을 추가해 특정 컬럼에서 NULL 값을 허용하지 않게 해줄 수 있다.

 

NULL 제약 조건 요구사항!

사용자 ( User ) 테이블

  • UserId 컬럼을 기본키로 설정한다.
  • 이름 ( name ) 컬럼을 가진다.

이외 요구사항

  • 사용자 테이블의 이름 컬럼은 NULL 값을 허용하지 않는다.
CREATE TABLE Users
(
    userId int(11)      NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name   varchar(255) NOT NULL
);

 


 

고유 제약 조건 ( Unique )

고유 제약 조건은 중복된 값을 허용하지 않도록 설정할 수 있다.

 

UNIQUE 제약 조건 요구사항!

사용자 ( User ) 테이블

  • userId 컬럼을 기본키로 설정한다.
  • 이름 ( name ) 컬럼을 가진다.

이외 요구사항

  • 사용자 테이블의 이름 컬럼은 NULL을 허용하지 않는다.
  • 사용자 테이블의 이름 컬럼은 중복된 값을 허용하지 않는다.
CREATE TABLE Users
(
    userId int(11)      NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name   varchar(255) NOT NULL UNIQUE
);

 

만약, 고유 제약 조건을 할당한 컬럼에 중복된 데이터를 삽입하면 어떻게 될까?

INSERT INTO Users (name) VALUES ('정성원');
INSERT INTO Users (name) VALUES ('정성원');
Error: ER_DUP_ENTRY: Duplicate entry '정성원' for key 'Users.name'

 

EP_DUP_ENTRY 에러메세지가 출력되고, 데이터가 삽입되지 않은 것을 확인할 수 있다.

기본키를 적용한 컬럼은 고유 제약 조건이 자동으로 적용된다.


 

외래 키 제약 조건 ( Foreign Key )

외래 키 ( Foreign Key )는 테이블간의 연관 관계 ( Relation Ship )를 표현할 때 사용한다.

연관 관계는 대표적으로 3가지의 형태로 표현할 수 있다.

  • 1:1 - 1명의 사용자 ( User ) 는 1개의 사용자 정보 ( UserInfo )를 가질 수 있다.
  • 1:N - 1명의 사용자 ( User )는 여러개의 주문 ( Order )을 할 수 있다.
  • N:M - 여러명의 학생 ( Student )은 여러개의 학원 ( School )을 등록할 수 있다.
CREATE TABLE 테이블명
  FOREIGN KEY (컬럼명) REFERENCES 참조_테이블명 (참조_컬럼명)
    ON DELETE [연계 참조 제약 조건]
    ON UPDATE [연계 참조 제약 조건]
);
  • 기본적으로, 외래 키 제약 조건은 CREATE TABLE 문을 사용해 테이블을 생성함과 동시에 정의한다.

 

외래키 제약 조건 요구사항!

정원 ( Garden ) 테이블

  • 이름 ( name ) 컬럼을 가진다.
  • 주소 ( address ) 컬럼을 가진다.

정원 식물 ( GardenPlants ) 테이블

  • 식물 이름 ( name ) 컬럼을 가진다.

이외 요구사항

  • 정원 식물 ( GardenPlants )은 특정 정원 ( Garden )에 소속되어 있다.
  • 하나의 정원은 여러개의 정원 식물을 가질 수 있다.
CREATE TABLE Garden
(
    gardenId INT(11)      NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name     VARCHAR(255) NOT NULL,
    address  VARCHAR(255) NOT NULL
);

CREATE TABLE GardenPlants
(
    gardenPlantsId INT(11)      NOT NULL AUTO_INCREMENT PRIMARY KEY,
    gardenId       INT(11)      NOT NULL,
    name           VARCHAR(255) NOT NULL,
    FOREIGN KEY (gardenId) REFERENCES Garden (gardenId)
);

 

 

위 예제에서 gardenId는 정원 식물 ( GardenPlants ) 테이블에서 정원 ( Garden ) 테이블을 참조하는 외래키다.

이것은 GardenPlants 테이블의 각 행이 실제로 존재하는 Garden 테이블의 행을 참조하도록 보장한다.

이처럼 외래키를 사용하여 특정 GardenPlants 테이블의 행이 어떤 Garden에 속해 있는지 알 수 있게 된다.

 

외래 키 ( Foreign Key ) 제약 조건 심화

외래 키의 경우 다른 테이블과 관계를 맺고 있는 참조 데이터가 삭제 ( DELETE ) 또는 수정 ( UPDATE )될 때 어떤 행위를 해야하는지 설정할 수 있다. 이런 행위를 수행하는 조건을 연계 참조 무결성 제약 조건 이라고 정의한다.

 

연계 참조 무결성 제약 조건의 종류

CASCADE

  • 참조하고 있는 개체가 변경 / 삭제 될 경우 함게 변경 / 삭제된다.
  • 예) 사용자가 삭제된다면, 그 사용자의 모든 주문 내역도 삭제 된다.
FOREIGN KEY (userId) REFERENCES Users(userId)
    ON DELETE CASCADE
    ON UPDATE CASCADE;

 

NO ACTION

  • 참조하고 있는 개체가 변경 / 삭제 될 경우 아무런 행위를 하지 않고 에러가 발생하게 된다.
  • 예) 사용자를 삭제할 때, 사용자의 주문 내역이 아직 존재한다면, 삭제를 막는다.
FOREIGN KEY (userId) REFERENCES Users(userId)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION;

 

SET NULL

  • 참조하고 있는 개체가 변경 / 삭제 될 경우 현재 데이터를 NULL로 변경한다.
  • 예) 사용자가 삭제되면, 사용자의 주문 내역의 '사용자 ID'는 NULL로 변경된다.
FOREIGN KEY (userId) REFERENCES Users(userId)
    ON DELETE SET NULL
    ON UPDATE SET NULL;

 

SET DEFAULT

  • 참조하고 있는 개체가 변경 / 삭제 될 경우 현재 데이터를 기본 값으로 변경한다.
  • 예) 사용자가 삭제되면, 사용자의 주문 내역의 '사용자 ID'는 기본 값으로 변경된다.
FOREIGN KEY (userId) REFERENCES Users(userId)
    ON DELETE SET DEFAULT
    ON UPDATE SET DEFAULT;

 


 

'데이터베이스' 카테고리의 다른 글

[DATABASE] ORM과 Prisma  (0) 2024.09.06
[DATABASE] Raw Query  (0) 2024.09.06
[DATABASE] SELECT JOIN 연산자  (0) 2024.09.05
[DATABASE] SQL ( Structured Query Language )  (0) 2024.09.05
[DATABASE] 관계형 데이터베이스  (0) 2024.09.05

SQL

데이터베이스에서 사용하는 생성, 삽입, 조회 명령문을 SQL ( Structured Query Language )라고 한다.

 

SQL의 종류

  • DDL ( Data Definition Language )
  • DML ( Data Manipulation Language )
  • DCL ( Data Control Language )
  • TCL ( Transaction Control Language )

DDL ( Data Definition Language )

데이터를 정의할 때 사용하는 언어로 테이블이나 데이터베이스를 생성, 수정, 삭제와 같은 행위를 할 때 사용한다.

  • CREATE ( DATABASE, TABLE, VIEW, INDEX 등을 생성할 때 사용한다. )
CREATE DATABASE 데이터베이스명;
CREATE TABLE 테이블명
{
 컬럼명 컬럼속성
}
  • DROP ( DATABASE, TABLE, VIEW, INDEX 등을 삭제할 때 사용한다. )
DROP DATABASE 데이터베이스명;
DROP TABLE 테이블명;
  • ALTER ( DATABASE, TABLE 등의 속성을 변경할 때 사용한다. )
ALTER DATABASE 데이터베이스명 변경조건;
ALTER TABLE 테이블명 ADD 변경조건; -- 테이블에 새로운 컬럼을 추가합니다.
ALTER TABLE 테이블명 DROP 변경조건; -- 테이블에서 컬럼을 삭제합니다.
ALTER TABLE 테이블명 MODIFY 변경조건; -- 테이블의 컬럼을 수정합니다.
ALTER TABLE 테이블명 RENAME 변경조건; -- 테이블의 이름을 변경합니다.
...

 


DML ( Data Manipulation Language )

데이터베이스에서 데이터를 조작할 때 사용하는 언어로 데이터의 저장, 삭제, 수정, 조회와 같은 행위를 한다.

  • SELECT ( 일반적으로 TABLE에서 원하는 데이터들을 조회할 때 사용한다. )
SELECT 컬럼목록 FROM 테이블명 [WHERE 조건];
  • INSERT ( TABLE에 새로운 데이터들을 삽입할 때 사용한다. ) 
INSERT INTO 테이블명 (컬럼목록) VALUES (값목록);

 ● DELETE

  • TABLE에서 특정한 조건에 맞는 데이터들을 삭제할 때 사용한다.
  • WHERE 조건이 없을 경우, 모든 데이터가 삭제 된다.
DELETE FROM 테이블명 [WHERE 조건];

 ● UPDATE

  • TABLE에서 특정한 조건에 맞는 데이터들을 수정할 때 사용한다.
  • WHERE 조건이 없을 경우, 모든 데이터가 수정된다
UPDATE 테이블명 SET 컬럼 = 값 [WHERE 조건];

 


 

DCL ( Data Control Language )

데이터베이스에 대한 권한과 관련된 문법으로 특정 유저가 DB에 접근할 수 있는 권한을 설정할 때 사용한다.

 ● GRANT

  • 데이터베이스의 특정한 유저에게 사용 권한을 부여할 때 사용한다.
  • 권한에는 SELECT, INSERT, UPDATE, DELETE 등 다양한 종류가 존재한다.
GRANT [권한] ON 객체명 TO 사용자;

 ● REVOKE

  • 데이터베이스의 특정한 유저에게 사용 권한을 취소할 때 사용한다.
REVOKE [권한] ON 객체명 FROM 사용자;

 


 

TCL ( Transaction Control Language )

데이터베이스 내의 트랜잭션을 관리하는 문법으로, 트랜잭션의 시작과 종료, 그리고 롤백을 수행할 때 사용한다.

 ● COMMIT

  • 데이터베이스의 작업이 정상적으로 완료되었음을 관리자에게 알려줄 때 사용한다.
START TRANSACTION;
...
COMMIT;

  ● ROLLBACK

  • 데이터베이스의 작업이 비정상적으로 완료되었음을 관리자에게 알려줄 때 사용한다.
  • 작업 중 오류가 발생한 경우 이를 취소하고, 이전 상태로 되돌릴 때 사용한다.
START TRANSACTION;
...
ROLLBACK;

 


 

'데이터베이스' 카테고리의 다른 글

[DATABASE] ORM과 Prisma  (0) 2024.09.06
[DATABASE] Raw Query  (0) 2024.09.06
[DATABASE] SELECT JOIN 연산자  (0) 2024.09.05
[DATABASE] SQL 제약조건  (0) 2024.09.05
[DATABASE] 관계형 데이터베이스  (0) 2024.09.05

RDB

 

관계형 데이터베이스 ( RDB )는 각 데이터를 '테이블'이라는 표 형태의 구조에 저장한다.

여기서, 각 '테이블'은 여러 정보를 저장하며, '테이블'간에 연관 관계를 설정해, 여러 테이블에 분산된 데이터를

서로 연결하여 관리할 수 있다.

  • 연관 관계 ( Relationship )란, 각 테이블 사이의 연결된 관계를 의미한다.
  • 관계형 데이터베이스는 테이블 ( Table )이라는 구조를 가지며, 각 테이블은 고유한 데이터를 나타내는 행 ( row )과 데이터의 속성을 나타내는 열 ( column )을 가진다.
  • 이 테이블들은 서로 연결될 수 있으며, 이 연결을 통해 복잡한 데이터를 관리할 수 있게 된다.

관계형 데이터베이스 ( RDB )와 비관계형 데이터베이스 ( NoSQL )의 개념

 

 

데이터 형식이 자유로운 비관계형 데이터베이스와는 달리 관계형 데이터베이스는 '테이블'이라는 개념이 존재한다.

여기서 테이블이란, 여러개의 '열( column )'과 '행( row )'을 가지는데, 이는 엑셀의 표와 유사한 형태다.

 

 

각 행 ( row )은 고유한 데이터 ( record )를 나타낸다. 각 열 ( column )은 해당 데이터의 속성 ( field )을 표현한다.

위 표를 예로 들어 설명하면

 행은 test@email.com, 테스트닉네임, 1234와 같은 정보를 나타내고,

 열은 이메일, 닉네임, 비밀번호와 같은 속성을 나타낸다.

 

주목해야할 점은 이 테이블 ( 표 )들 간에 서로 연관 관계 ( Relationship )를 가질 수 있다는 점이다.

이러한 관계를 통해 더욱 복잡한 쿼리를 작성할 수 있고, 바로 이 점이 "관계형 데이터베이스"라는 이름의 유래가 된것.

 

그렇다고 해서 비관계형 데이터베이스 ( NoSQL )가 좋지 않다는 것은 아니다. NoSQL은 유연한 데이터 구조를 가지기 때문에, 저장과 읽기 작업이 더욱 빠르고, 복잡한 비즈니스 로직 없이 주로 데이터 읽기와 쓰기에 중점을 둔 서버에서 주로 사용한다. ( 주로 빅데이터 환경, 단순 페이지뷰가 많은 앱 등 )

반면, 관계형 데이터베이스 ( RDB )는 더욱 복잡한 비즈니스 로직과 정형화된 데이터를 체계적으로 관리할 수 있어 더욱 안전한 서버 환경을 구성하기에 좋다. ( 보안이 중요한 기관, 은행과 같은 안전성을 중시하는 회사 등 )

'데이터베이스' 카테고리의 다른 글

[DATABASE] ORM과 Prisma  (0) 2024.09.06
[DATABASE] Raw Query  (0) 2024.09.06
[DATABASE] SELECT JOIN 연산자  (0) 2024.09.05
[DATABASE] SQL 제약조건  (0) 2024.09.05
[DATABASE] SQL ( Structured Query Language )  (0) 2024.09.05

+ Recent posts