오늘의 목표

더보기

✔️ 프로그래머스 코테 문제 풀기

✔️ 팀 프로젝트 ( 회원가입, 로그인 )


⏱️ 오늘의 일정

9:00 ~ 10:00 - 프로그래머스 코테 문제 풀기
10:00 ~ 11:00 - 스탠다드반 CS 강의

11:00 ~ 21:00 - 팀 프로젝트

19:00 ~ 20:00 - 챌린지반 수업 

 


📜 프로그래머스 코테 문제 풀기

9:00 ~ 10:00 - 프로그래머스 코테 문제 풀기

 

대충만든 자판

 

https://github.com/YamSaeng/AlgorithmCodingTest/tree/main/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4/1/160586.%E2%80%85%EB%8C%80%EC%B6%A9%E2%80%85%EB%A7%8C%EB%93%A0%E2%80%85%EC%9E%90%ED%8C%90

 

AlgorithmCodingTest/프로그래머스/1/160586. 대충 만든 자판 at main · YamSaeng/AlgorithmCodingTest

This is an auto push repository for Baekjoon Online Judge created with [BaekjoonHub](https://github.com/BaekjoonHub/BaekjoonHub). - YamSaeng/AlgorithmCodingTest

github.com

 

function solution(keymap, targets) {
    var answer = [];    
    for (let i = 0; i < targets.length; i++) {                
        answer.push(0);
        for (let j = 0; j < targets[i].length; j++) {
            const targetChar = targets[i][j];
            let minBtnClick = 1000;            
            let findChar = false;
            for (let k = 0; k < keymap.length; k++) {
                for (let l = 0; l < keymap[k].length; l++) {
                    if (keymap[k][l] == targetChar) {
                        if (l < minBtnClick) {
                            minBtnClick = l + 1;
                            findChar = true;
                        }
                    }
                }
            }
            if (findChar == false) {
                answer[i] = -1;
                break;
            }
            
            answer[i] += minBtnClick;
        }        
    }    
    return answer;
}

 

문제 설명에 있는 대로 반복문을 사용해 구현했다.

반복문이 많아서 시간초과가 날까 싶었는데, 초과하는 예시는 없었다.

targets 의 개수 만큼 answer에 0을 넣고 해당 값을 증가시켜주는 방식으로 구현했다.

 

 

📜 스탠다드반 CS 강의

9:00 ~ 10:00 - 스탠다드반 CS 강의

 

IP 주소와 클래스풀 주소 체계에 대한 내용과 서브넷 마스크를 배웠다.

스탠다드반 수업내용은 따로 정리해 포스팅하고 링크를 남겨야겠다.

 

 

📜 팀 프로젝트

11:00 ~ 21:00 - 팀 프로젝트

 

본격적인 팀 프로젝트 구현 첫 날을 맞았다.

우선 기초적인 회원가입과 로그인을 구현해 Push 했다.

 

회원가입 api

// 회원가입
usersRouter.post('/Sign-Up', async (req, res, next) => {
    const { name, nickname, id, password, confirmPassword } = req.body;
    const isExistUser = await prisma.users.findFirst({
        where: {
            id: id
        }
    });

    if (isExistUser) {
        return res.status(404).json({ message: `이미 존재하는 아이디입니다.` });
    }

    if (confirmPassword === undefined) {
        return res.status(404).json({ message: `비밀번호 확인을 입력하세요` });
    }

    if (password !== confirmPassword) {
        return res.status(404).json({ message: `비밀번호와 비밀번호 확인이 일치하지 않습니다.` });
    }

    const hashedPassword = await bcrypt.hash(password, 10);

    // 유저 생성해서 Users table에 저장
    const user = await prisma.users.create({
        data: {
            name: name,
            nickname: nickname,
            id: id,
            password: hashedPassword
        }
    });

    // 랭킹 생성해서 Ranking table에 저장
    const rank = await prisma.ranking.create({
        data: {
            userId: user.userId
        }
    });

    // 스쿼드 생성해서 Squad table에 저장
    const squad = await prisma.squad.create({
        data: {
            userId: user.userId
        }
    });

    return res
        .status(201)
        .json({ message: `${id}로 회원가입이 완료되었습니다.` });
});

 

회원가입은 위 코드에서 추가할 부분이 없어보이기는 한다.

아마 닉네임 중복 점검 정도 추가하지 않을까 싶다.

 

 

 

로그인 api

usersRouter.post('/Sign-In', async (req, res, next) => {
    // 아이디, 비밀번호 가져오기
    const { id, password } = req.body;
    // AccessToken이 있는지 확인
    const { authorization } = req.headers;

    // userDB에 아이디가 있는지 확인
    const user = await prisma.users.findFirst({
        where: {
            id: id
        }
    });

    // 아이디 검사
    if (!user) {
        return res.status(404).json({ message: `${id}은 존재하지 않는 아이디 입니다.` });
    }

    // 비밀번호 검사
    if (!(await bcrypt.compare(password, user.password))) {
        return res.status(404).json({ message: `비밀번호가 일치하지 않습니다.` });
    }

    // JWT로 AccessToken 생성
    const s2cAccessToken = CreateAccessToken(id);
    // JWT로 RefreshToken 생성
    const s2cRefreshToken = CreateRefreshToken(id);   

    // 응답 헤더에 accessToken 기록
    res.header("authorization", s2cAccessToken);

    return res.status(200).json({ message: `${id}로 로그인에 성공했습니다.` });
})

 

로그인은 기본적으로 jwt로 accesstoken을 발급하고, 헤더에 저장해 발급해주는 방법을 우선 사용했다.

테스트를 해야하기 때문에 인증은 간단하게!

 

로그인은 RefreshToken을 활용해 로직을 구현하려고 한다.RefreshToken을 DB에 저장해 관리한다.AccessToken이 만료되면, RefreshToken으로 AccessToken의 재발행 여부를 판단한다.

 

 

 

📜 챌린지반 수업

19:00 ~ 20:00 - 챌린지반 수업 

 

오늘 챌린지반 수업에서는 DB Query 중 집계함수와 그룹핑에 대한 내용을 배웠다.

추가로 숙제를 내줬는데 프로그래머스 SQL 문제 4개를 풀어오는 숙제다.

 

랜덤하게 챌린지반 수강생 중 3명을 골라 발표를 시킨다고 공지를 받았다.

SQL 문제를 풀고, TIL에 기록해야겠다.

 

 

Visual Studio Code 디버그 해보기

디버그 탭

 

위 그림 빨간줄에 있는 launch.json 파일 만들기를 누른다

 

 

누르면 위 그림이 가운데 위에 뜨는데 Node.js를 선택한다.

 

launch.json 파일

 

Node.js를 선택하면 위 그림처럼 launch.json 파일이 만들어지는데, 밑줄 친 부분에 진입 부분 .js 파일의 경로를 입력한다.

 

 

중단점을 찍고 F5 번을 눌러 디버그에 진입하면 잘 잡히는 것을 볼 수 있다. 오예!

'IT' 카테고리의 다른 글

[IT] 객체 지향 설계 5 원칙 ( SOLID )  (1) 2024.09.27
[IT] 객체 지향 프로그래밍 ( Object-Oriented Programming, OOP )  (0) 2024.09.26
[IT] 정규표현식  (1) 2024.09.10
[IT] Access Token, Refresh Token  (1) 2024.09.09
[IT] 인증, 인가  (0) 2024.09.09

 

express-session은 Express.js에서 세션 ( Session ) 기능을 쉽게 구현하기 위한 미들웨어다.

세션 ( Session )을 사용하기 위해서는 사용자 인증세션 스토리지를 통해 사용자 정보를 저장하고,

세션 정보가 담긴 쿠키를 사용자에게 발급하는 과정이 필요하다.

express-session은 이런 복잡한 과정을 생략해 간단하게 세션 기능을 구현할 수 있도록 도와주는 미들웨어다.

 

express-session 시작해보기

express-session은 세션 ID를 클라이언트에게 발급하고, 이 세션 ID를 통해 서버는 클라이언트의 상태를 추적할 수 있다.

즉, 클라이언트가 세션 ID를 발급받은 후에는 모든 서버 요청마다 세션 ID가 포함된 쿠키를 전달하게 되고, 이로 인해 서버는 클라이언트를 쉽게 식별할 수 있게 된다. ( Session 객체에 세션 ID를 저장하는 방법과 동일한 방법 )

 

예시를 통해 express-session의 동작 방식을 확인해보자

 

설치

# express, express-session를 설치합니다.
yarn add express express-session

 

app.js

// app.js

import express from 'express';
import expressSession from 'express-session';

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

app.use(express.json());
app.use(
  expressSession({
    secret: 'express-session-secret-key.', // 세션을 암호화하는 비밀 키를 설정
    resave: false, // 클라이언트의 요청이 올 때마다 세션을 새롭게 저장할 지 설정, 변경사항이 없어도 다시 저장
    saveUninitialized: false, // 세션이 초기화되지 않았을 때 세션을 저장할 지 설정
    cookie: {
      // 세션 쿠키 설정
      maxAge: 1000 * 60 * 60 * 24, // 쿠키의 만료 기간을 1일로 설정합니다.
    },
  }),
);

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

 

express-session은 아래와 같이 전역 미들웨어로 등록된다.

app.use(
  expressSession({
    secret: 'express-session-secret-key.', // 세션을 암호화하는 비밀 키를 설정
    resave: false, // 클라이언트의 요청이 올 때마다 세션을 새롭게 저장할 지 설정, 변경사항이 없어도 다시 저장
    saveUninitialized: false, // 세션이 초기화되지 않았을 때 세션을 저장할 지 설정
    cookie: {
      // 세션 쿠키 설정
      maxAge: 1000 * 60 * 60 * 24, // 쿠키의 만료 기간을 1일로 설정합니다.
    },
  }),
);

각각의 구성요소는 아래와 같은 내용을 담는다.

 

secret

  • 세션 ID를 암호화하기 위한 비밀 키
  • 클라이언트에게 발급할 세션 ID를 암호화하기 위한 비밀 키 정보

resave

  • 클라이언트의 요청( Request )이 들어올 때마다 세션 정보를 다시 저장할 지 설정한다.
  • 변경사항이 없더라도 true로 설정하면, 매번 새로운 세션에 저장된다.

saveUninitialized

  • req.session에 아무런 정보가 저장이 되지 않아도 사용자에게 세션 ID를 발급할지 설정한다.
  • true로 설정하면, 서버에 접속하는 모든 사용자에게 세션 ID가 발급된다.

cookie.maxAge

  • 세션 ID가 저장된 클라이언트의 쿠키 만료 기간을 설정한다.

 

express-session 설정 정보

https://github.com/expressjs/session#options

 

GitHub - expressjs/session: Simple session middleware for Express

Simple session middleware for Express. Contribute to expressjs/session development by creating an account on GitHub.

github.com

 

express-session API 만들기

POST /sessions API를 호출했을 때 전달 받은 userId를 세션에 저장하고, GET /sessions API를 호출했을 때 클라이언트의 세션 정보를 출력하는 API를 구현해보자.

 

POST /sessions API 만들기

/** 세션 등록 API **/
app.post('/sessions', (req, res, next) => {
  const { userId } = req.body;

  // 클라이언트에게 전달받은 userId를 세션에 저장합니다.
  req.session.userId = userId;

  return res.status(200).json({ message: '세션을 설정했습니다.' });
});
  • req-session은 클라이언트의 세션 정보를 관리하는 데 사용되는 객체다.
  • 클라이언트의 요청이 들어오면, req.session.userId에 원하는 정보를 저장한다.
  • userId 대신 다른 이름을 사용하려면 req.session.<원하는 프로퍼티 명>의 형식으로 사용하면 된다.

GET /sessions API 만들기

/** 세션 조회 API **/
app.get('/sessions', (req, res, next) => {
  return res.status(200).json({
    message: '세션을 조회했습니다.',
    session: req.session.userId ?? null, // 세션에 저장된 usrId를 조회합니다.
  });
});
  • express-session은 클라이언트가 전달한 쿠키의 세션 ID를 바탕으로 req.session에서 정보를 조회한다.
  • 만약, 클라이언트가 제공한 세션 ID에 일치하는 세션이 없을 경우, null을 반환한다.

express-session은 클라이언트의 요청 ( Request )에 req.session 정보를 저장할 수 있고, 이후 클라이언트 요청에서 세션에 저장된 정보를 req.session에서 참조해 사용할 수 있게 된다.

이 방식은 기존에 JWT를 이용한 쿠키를 클라이언트에게 전달하는 것보다 더욱 편리하게 구현할 수 있다는 점이 가장 큰 장점이다.

 

express-session의 정보는 서버가 종료되면 사라지는 가장 큰 문제가 존재한다. 이는 세션 정보가 인 메모리방식으로 저장되기 때문인데, 이로 인해 서버가 재시작 되거나 중지될 때 마다 모든 세션 정보가 사라지게 된다.

이런 상황을 방지하기 위해, Redis와 같은 캐시 메모리 데이터베이스를 이용해 세션 정보를 영구적으로 저장해 관리하기도 한다.

 

테스트해보기

1) Insomnia에서 Http Request를 생성하고, POST /sessions API를 호출

userId를 임의로 지정해 서버로 전달

 

세션을 설정했습니다라는 res를 받는다.

 

세션 ID가 저장된 connect.sid 이름을 가지는 Cookie를 전달받는다. ( 이 쿠키는 클라이언트의 세션 ID를 가지고 있다. )

 

2) Insomnia에서 Http Request를 생성하고, GET /sessions API를 호출

 

GET /sessions API를 호출한다.

 

세션 ID를 이용해 서버에 저장된 userId를 확인할 수 있다.

  • GET /sessions API를 호출하면, "세션을 조회했습니다"라는 응답을 확인할 수 있다.
  • 서버는 클라이언트가 전달한 connect.sid 쿠키에 저장된 세션 ID를 바탕으로 서버의 세션 정보를 조회하게 된다.

배열 - 메모리상에 원소를 연속하게 배치한 자료구조다.

 

배열의 성질

  • O(1)에 k번째 원소를 확인 및 변경이 가능
  • 추가적으로 소모되는 메모리의양( =overhead )가 거의 없다.
  • 메모리 상에 데이터들이 붙어있으므로 캐시 적중률이 높다
  • 메모리 상에 연속한 구간을 잡아야 하므로 할당에 제약이 걸린다.

 

기능과 구현

  • 임의의 위치에 있는 원소를 확인 하거나 변경할때 O(1)이 걸린다.
  • 원소를 끝에 추가 = O(1)
  • 마지막 원소를 제거 = O(1)
  • 임의의 위치에 원소를 추가 / 임의 위치의 원소 제거 = O(N)

 

'알고리즘' 카테고리의 다른 글

[알고리즘] DFS  (0) 2024.11.11
[알고리즘] 힙 정렬 ( Heap Sort )  (0) 2024.11.10
[알고리즘] 합병 정렬  (0) 2024.08.28
[알고리즘] 퀵 정렬  (0) 2024.08.28
[알고리즘] 삽입 정렬  (0) 2024.08.28

오늘의 목표

더보기

✔️ 프로그래머스 코테 문제 풀기

✔️ 팀 프로젝트 과제 발제

✔️ 팀 회의


 

⏱️ 오늘의 일정

9:00 ~ 10:00 - 프로그래머스 코테 문제 풀기
10:00 ~ 11:00 - 팀 프로젝트 과제 발제

11:00 ~ 20:00 - 팀 회의


 

📜 프로그래머스 코테 문제 풀기

9:00 ~ 10:00 - 프로그래머스 코테 문제 풀기

 

문자열 나누기

 

https://github.com/YamSaeng/AlgorithmCodingTest/tree/main/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4/1/140108.%E2%80%85%EB%AC%B8%EC%9E%90%EC%97%B4%E2%80%85%EB%82%98%EB%88%84%EA%B8%B0

 

AlgorithmCodingTest/프로그래머스/1/140108. 문자열 나누기 at main · YamSaeng/AlgorithmCodingTest

This is an auto push repository for Baekjoon Online Judge created with [BaekjoonHub](https://github.com/BaekjoonHub/BaekjoonHub). - YamSaeng/AlgorithmCodingTest

github.com

 

문제를 한번에 이해를 못해서 여러번 읽고 이해한 후,

문제 설명에 맞게 코드를 짜니 쉽게 해결했다.

function solution(s) {
    let answer = 0;
    let isX = 0;
    let notX = 0;
    let x = "";

    for (let i = 0; i < s.length; i++) {
        if (x == "") {
            x = s[i];
            isX++;
            continue;
        }

        if (x == s[i]) {
            isX++;
        }            
        else {
            notX++;
        }            

        if (isX == notX) {
            answer++;
            isX = 0; notX = 0;
            x = "";
        }
    }

    if (isX != 0) {
        answer += 1;
    }        

    return answer;
}

 

 

📜 팀 과제 발제

10:00 ~ 11:00 - 팀 과제 발제

 

총 12일 간의 시간 기한으로 한 팀 과제가 발제되었다.

이번 팀 과제는 풋살 게임의 서버를 만드는 과제다.

 

 

필수 구현 기능은 다음과 같다.

 

1. 회원가입 / 로그인 기능

2. 캐시 구매 기능

3. 선수 데이터 준비

4. 선수 뽑기 기능

5. 나만의 팀 꾸리기 기능

6. 축구 게임 기능

 

도전 기능은 다음과 같다.

 

1. 승리 / 패배 시 게임 점수 조정 기능

2. 유저 랭킹 조회 기능

3. 점수 기반 자동 매치 메이킹 기능

4. 선수 강화 기능

 

제출은 9 / 25일 수 오전 12:00로 정해졌다.

 

 

📜  팀 회의

11:00 ~ 21:00 - 팀 회의

 

S.A를 작성해 21:00 까지 튜터님한테 피드백을 받으라는 공지가 있어서, 팀원들과 함께 S.A를 작성했다.

 

1. 와이어 프레임

 

회원가입 화면

 

로그인 화면

 

인벤토리 확인 및 뽑기 화면

 

대상 지정 게임

 

 

레이팅 게임

 

강화 화면

 

2. API 명세서

 

API 명세서를 팀원들끼리 작성하면서 서로 업무를 분담했다.

내가 맡은건 회원가입과 로그인, 그리고 전체 랭킹 조회, 로그를 맡았다.

 

 

3. 데이터 베이스 관계를 ERD DIAGRAM을 이용해 작성했다.

 

오늘의 목표

더보기

✔️ 스탠다드반 수업

✔️ 개인 과제 마무리

✔️ 데이터 베이스개념, 인증, 인가 특강

✔️ 챌린지반 수업


⏱️ 오늘의 일정

10:00 - 11:00 - 스탠다드 반 수업
11:00 ~ 12:00 - 개인 과제 마무리
 

12:00 ~ 13:00 - 개인 과제 다듬기

13:00 ~ 14:00 - 점심시간

14:00 ~ 16:00 - 데이터 베이스개념, 인증, 인가 특강

16:00 ~ 18:00 - 개인 과제 다듬기

18:00 ~ 19:00 - 저녁시간

19:00 ~ 20:00 - 챌린지반 수업20:00 ~ 21:00 - 팀원 코드 리뷰


📜 스탠다드 반 수업

10:00 - 11:00 - 스탠다드 반 수업

 

오늘 수업은 발제자분들의 발표로만 이루어졌다.

OSI 7계층 중 3계층인 네트워크 계층에 대한 주제로 발표가 시작되었다.

 

 

📜 개인 과제 마무리

11:00 - 12:00 - 개인과제 마무리

 

 

길고 길었던 개인과제를 마무리했다.

개인과제를 마무리하고 미처 구현하지 못했던 기능들도 구현해 추가로 제출했다.

 

아이템 구입할때 발견하지 못했던 부분을 발견해서 버그를 수정했다.

 

 

 

 // 캐릭터의 인벤토리를 찾는다.
 const chracterInventory = await prisma.inventory.findFirst({
     where: {
         characterId: character.characterId
     }
 });
 
 // 인벤토리에서 비어있는 공간을 찾는다.
 const emptyInventoryItem = await prisma.inventoryItem.findFirst({
     where: {
         inventoryId: chracterInventory.inventoryId,
         itemId : 0
     }
 });

 // 비어있는 공간에 아이템을 넣는다.
 await prisma.inventoryItem.update({
     where: {
         inventoryItemIndex: emptyInventoryItem.inventoryItemIndex,
         inventoryId : chracterInventory.inventoryId
     },
     data: {
         itemId: searchItem.itemId,
         inventoryItemCount: count
     }
 })

 

위 코드는 아이템을 구입하고 인벤토리에 넣을 때, 인벤토리에서 빈 공간을 찾은 후 해당 공간에 아이템을 넣는 코드다.

비어있는 공간에 아이템을 넣는 부분의 로직은 다음과 같다.

 

인벤토리 아이템을 업데이트 하는데, 인벤토리 아이템의 위치값이 위에서 찾은 비어있는 공간의 아이템의 위치값과 같고,해당 인벤토리 아이템이 캐릭터가 소유하고 있는 인벤토리일 경우,해당 인벤토리 아이템의 아이템 코드와 개수를 업데이트 한다.

 

이처럼 구성을하고 실행을 하니 에러가 났다.

 

 

에러 내용을 살펴보니 inventoryitemId를 활용하라는 의미의 버그였다.

생각해봐도 왜 굳이 저거를 써야할까..?

로우 쿼리 자체가 문제가 되는가 싶어서 MySQL Workbench에서 위 로직으로 쿼리를 짜서 실행해보니 문제 없이 실행됐다.

 

Prisma 자체의 문제인가 싶어서 튜터님에게 찾아가 문의를 하니, Prisma에서는 PK로 설정한 값을 활용해 where문을 작성해야한다는 말을 들었다.

 // 비어있는 공간을 찾는다.
 const emptyInventoryItem = await prisma.inventoryItem.findFirst({
     where: {
         inventoryId: chracterInventory.inventoryId,
         itemId: 0
     }
 });

// 비어있는 공간에 아이템을 넣는다.
await prisma.inventoryItem.update({
    where: {
        inventoryItemId: emptyInventoryItem.inventoryItemId                    
    },
    data: {
        itemId: searchItem.itemId,
        inventoryItemCount: count
    }
});

 

그래서 위처럼 수정을 했다.

 

인벤토리 아이템을 업데이트 하는데,

인벤토리 아이템의 ID 값이 비어있는 아이템의 ID 값이 같은 대상을 찾고,

해당 인벤토리 아이템의 아이템 코드와 개수를 업데이트한다.

 

이처럼 구성을하고 실행을 하니 깔끔하게 업데이트가 되었다.

위처럼 구성하는 것이 기본적인 방법이라고 튜터님에게서 조언을 받았다.

기본 ID 값을 활용해 업데이트 하는 방식으로 SQL 쿼리를 만들어야겠다.

 

📜 데이터베이스 개념, 인증 인가 특강

14:00 ~ 16:00 - 데이터 베이스개념, 인증, 인가 특강

 

앞서 노드 강의에서 배운 데이터베이스의 개념과 인증, 인가에 정리 차원에서 특강 수업이 있어서 들었다.

 

📜 챌린지반 수업

19:00 - 20:00 - 챌린지 반 수업

 

매주 화 목 진행하는 챌린지반 수업에 참여했다.

이번 강의에서는 RDBMS에 대한 개념을 주제로 해서 수업이 이루어졌다. 

중간 중간 쿼리에 대한 문제를 내주고 푸는 짧은 문제 풀이 시간도 있었다.

 

📜 팀원 코드 리뷰

20:00 - 21:00 - 팀원 코드 리뷰

 

20시 부터 팀원 코드 리뷰 시간이 있어서 참여했다.

다른 분들의 코드를 보면서 의아한 부분은 질문하고, 도움이 될만한 부분을 발견해 많은 도움이 되었다.

 

https://dbdiagram.io/home

 

dbdiagram.io - Database Relationship Diagrams Design Tool

 

dbdiagram.io

 

무료로 DB 다이어그램을 구성할 수 있는 사이트를 공유 받아서 앞으로 자주 이용해야겠다.

 

🌙 하루를 마치며

오늘로 개인과제가 마무리 되었다.

9월 13일 오전 10시에 팀 과제 발제가 있다고 공지를 받았다.

어떤 문제가 나올지 기대되고, 팀원 코드 리뷰를 하면서 팀 과제 분배를 어떻게 할지도 상의했다.

우선 지금 생각으로는 개인과제를 하며 발견한 router 단위로 분배를 하는법

아니면 어떤 큼직한 로직 담당으로 분배를 하는법 2가지 정도를 생각하고 의견을 나눴다.

 

최종으로는 팀 과제 발제를 듣고나서 적절하게 분배해 수행하는 것으로 이야기를 마루리햇다.

오늘의 목표

더보기

✔️ 프로그래머스 코테 문제 풀기

✔️ 개인과제

✔️ Node.js 강의 듣기


⏱️ 오늘의 일정

9:00 ~ 10:00 - 프로그래머스 코테 문제 풀기
10:00 ~ 13:00 - Node.js 강의 듣기

13:00 ~ 14:00 - 점심시간

14:00 ~ 20:00 - Node.js 강의 듣기

20:00 ~ 21:00 - 개인과제 기본 완료


📜 프로그래머스 코테 문제 풀기

9:00 ~ 10:00 - 프로그래머스 코테 문제 풀기

 

숫자 짝꿍 문제를 풀었다. 

 

function solution(X, Y) {
    let newSet = [];
    var answer = '';

    let findIndex = [];    

    for (let i = 0; i < X.length; i++) {        
        for (let j = 0; j < Y.length; j++) {
            if (X[i] === Y[j]) {
                let duplicateIndex = false;
                for (let k = 0; k < findIndex.length; k++) {
                    if (findIndex[k] == j) {
                        duplicateIndex = true;
                        break;
                    }
                }

                if (duplicateIndex == false) {
                    newSet.push(X[i]);
                    findIndex.push(j);
                }                
            }
        }
    }

    let answerArray = Array.from(newSet);
    answerArray.sort((a, b) => b - a);

    if (answerArray.length == 0) {
        return "-1";
    }
    else if (answerArray[0] == 0) {
        return "0";
    }
    else {
        return answerArray.join("");
    }    
}

 

 

📜 Node.js 강의 듣기

10:00 ~ 20:00  - Node.js 강의 듣기

 

드디어 길고 길었던, Node.js 강의를 모두 다 들었다.

한 강의당 시간이 꽤 길고, 블로그에 정리하면서 이해안되는 부분은 구글로 찾고 하느라 시간이 매우 오래걸렸다. ㅠㅠ

이번주 남은 시간 동안 다시 한번 복습해 빠진 부분이 있으면 추가로 글을 작성해야겠다.

 

2024.09.11 - [Javascript] - [Javascript] 로그 ( Log ), 에러 처리 ( Error Handling ) 미들웨어

 

[Javascript] 로그 ( Log ), 에러 처리 ( Error Handling ) 미들웨어

로그 미들웨어 ( Log Middleware )는 클라이언트의 모든 요청 사항을 기록해 서버의 상태를 모니터링하기 위한 미들웨어다. 로그 미들웨어는 클라이언트의 요청을 기록해 어플리케이션을 모니터링

program-yam.tistory.com

 

2024.09.11 - [내일배움캠프/실습] - [Javascript][실습][게시판 프로젝트] 게시글 API

 

[Javascript][실습][게시판 프로젝트] 게시글 API

게시글 생성 API 비즈니스 로직게시글을 작성하려는 클라이언트가 로그인된 사용자인지 검증한다.게시글 생성을 위한 title, content를 body로 전달받는다.Posts 테이블에 게시글을 생성한다. 게시글

program-yam.tistory.com

 

2024.09.11 - [데이터베이스] - [DATABASE] 트랜잭션 ( Transaction )

 

[DATABASE] 트랜잭션 ( Transaction )

트랜잭션 ( Transaction )은 작업의 완전성을 보장해주기 위해 사용되는 개념이다.특정한 작업을 전부 처리하거나, 전부 실패하게 만들어 데이터의 일관성을 보장해주는 기능이다. 트랜잭션 ( Transa

program-yam.tistory.com

 

2024.09.11 - [데이터베이스] - [DATABASE] Prisma Transaction ( [게시판 프로젝트] 회원가입 적용, 사용자 히스토리 테이블 생성해 적용해보기 )

 

[DATABASE] Prisma Transaction ( [게시판 프로젝트] 회원가입 적용, 사용자 히스토리 테이블 생성해 적용

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

program-yam.tistory.com

 

트랜잭션에 대해 다시 한번 개념을 이해하고, 실습을 통해 더욱 더 이해를 하게 되었다.

 

 

📜 개인과제 기본 완료

20:00 ~ 21:00 - 개인과제 기본 완료

 

 

개인 과제 기본을 완료했다. 내일 목요일 ( 9/12 ) 정오까지 제출이긴 한데...

쪽잠을 자더라도 도전 과제를 구현하고 자야겠다.

 

어제 의문점을 가졌던 로그인에서의 AccessToken과 RefreshToken의 사용방법에 대한 질문을 튜터님에게 했다.

로그인 단계에서는 보통 안전하다고 믿고 가는 경우도 있고, 내가 의문을 가졌던 RefreshToken이 변조되었을 경우에 해당 사용자를 거부 할 수도 있고 이처럼 회사마다 방식이 각각 다르다고 전달을 받았다.

 

지금 현재 내코드에서는 무조건 아이디와 비밀번호만 같다면 로그인 성공처리를 해주기는 하는데, 로그인 거부 할 수 있는 방법을 찾아 거부 할 수 있도록 해야겠다.

 

아이템 수정

// 아이템 수정
router.post('/item-edit/:itemCode', async (req, res, next) => {
    const { itemCode } = req.params;
    const editData = req.body;

    // 아이템을 찾음
    const searchItem = await prisma.items.findFirst({
        where: {
            itemCode: +itemCode
        }
    });

    // 못찾으면 에러 반환
    if (!searchItem) {
        return res
            .status(401)
            .json({ message: `수정할 아이템이 서버에 존재하지 않습니다.` });
    }

    // 가격 속성 삭제해서 가격 수정할 수 없게 함
    delete editData.itemPrice;

    // 아이템 나머지 능력치가 정해지지 않을 경우 각 데이터를 0으로 초기화
    if (editData.itemStr === undefined) {
        editData.itemStr = 0;
    }

    if (editData.itemDex === undefined) {
        editData.itemDex = 0;
    }

    if (editData.itemInt === undefined) {
        editData.itemInt = 0;
    }

    // item 테이블 업데이트
    await prisma.items.update({
        where: {
            itemCode: +itemCode
        },
        data: {
            ...editData
        }
    });

    return res
        .status(200)
        .json({ message: `${itemCode} 아이템 수정 성공` });
});

 

아이템 전체 조회

// 아이템 전체 조회
router.get('/item', async (req, res, next) => {
    // itemCode를 기준값으로 해서 오름차순으로 정렬해 데이터를 가져옴
    const allItems = await prisma.items.findMany({
        select: {
            itemCode: true,
            itemName: true,
            itemPrice: true,
            itemStr: true,
            itemDex: true,
            itemInt: true
        },
        orderBy: {
            itemCode: 'asc'
        }
    });

    // 조회한 전체 아이템 반환
    return res.status(200).json({ allItems });
});

 

특정 아이템 조회

// 특정 아이템 조회
router.get('/item/:itemCode', async (req, res, next) => {
    const { itemCode } = req.params;

    // itemCode로 item 테이블에서 아이템 찾음
    const item = await prisma.items.findFirst({
        where: {
            itemCode : +itemCode
        },
        select: {
            itemCode: true,
            itemName: true,
            itemPrice: true,
            itemStr:true,
            itemDex: true,
            itemInt:true,
        }
    })

    // 아이템을 찾지 못하면 에러 반환
    if (!item) {
        return res
            .status(401)
            .json({ message: '조회하고자 하는 아이템을 찾을 수 없습니다.' });
    }

    // 찾은 아이템 정보 반환
    return res
        .status(200)
        .json({ item });
});

 

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 )라고 부른다.

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

  1. 게시글을 작성하려는 클라이언트가 로그인된 사용자인지 검증한다.
  2. 게시글 생성을 위한 title, content를 body로 전달받는다.
  3. Posts 테이블에 게시글을 생성한다.

 

게시글은 사용자 ( Users )는 여러개의 게시글 ( Posts )을 등록할 수 있다는 조건에 따라 사용자와 1:N의 관계를 가지고,

현재 로그인 한 사용자의 정보가 존재했을 때만 게시글을 생성할 수 있도록 구현해야 한다.

 

사용자를 생성했을 때와 동일하게, routers/posts.router.js 파일을 생성하고,  app.js 파일에 라우터를 추가해보자.

 

게시글 생성 API

// src/routes/posts.router.js

import express from 'express';
import { prisma } from '../utils/prisma/index.js';
import authMiddleware from '../middlewares/auth.middleware.js';

const router = express.Router();

/** 게시글 생성 API **/
router.post('/posts', authMiddleware, async (req, res, next) => {
  const { userId } = req.user;
  const { title, content } = req.body;

  const post = await prisma.posts.create({
    data: {
      userId: +userId,
      title,
      content,
    },
  });

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

export default router;

 

사용자 인증 미들웨어를 통해 게시글을 작성하려는 클라이언트가 로그인된 사용자 인지 검증하였고,

전달된 userId 값을 이용해, 사용자와 1:N 관계를 맺고 있는 게시글을 생성하도록 구현했다.

 

PostsRouter를 등록한 app.js

// src/app.js

import express from 'express';
import cookieParser from 'cookie-parser';
import LogMiddleware from './middlewares/log.middleware.js';
import ErrorHandlingMiddleware from './middlewares/error-handling.middleware.js';
import UsersRouter from './routes/users.router.js';
import PostsRouter from './routes/posts.router.js';

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

app.use(LogMiddleware);
app.use(express.json());
app.use(cookieParser());
app.use('/api', [UsersRouter, PostsRouter]);
app.use(ErrorHandlingMiddleware);

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

 

게시글 조회 API

게시글 목록 조회 API와 게시글 상세 조회 API는 이전에 prisma-curd에서 구현한 Posts 테이블과 동일한 코드로 구성되어 있다.

 

게시글 목록 조회 API

// src/routes/posts.router.js

/** 게시글 목록 조회 API **/
router.get('/posts', async (req, res, next) => {
  const posts = await prisma.posts.findMany({
    select: {
      postId: true,
      userId: true,
      title: true,
      createdAt: true,
      updatedAt: true,
    },
    orderBy: {
      createdAt: 'desc', // 게시글을 최신순으로 정렬합니다.
    },
  });

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

 

 

게시글 상세 조회 API

// src/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,
      userId: true,
      title: true,
      content: true,
      createdAt: true,
      updatedAt: true,
    },
  });

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

+ Recent posts