1. Prisma의 Transaction
Prisma의 트랜잭션은 여러 개의 쿼리를 하나의 트랜잭션으로 수행할 수 있는
Sequential 트랜잭션과 Prisma가 자체적으로 트랜잭션의 성공과 실패를
관리하는 Interactive 트랜잭션이 존재합니다.
2. Sequential 트랜잭션
- Sequential 트랜잭션 1
import { PrismaClient } from '@prisma/client' ;
const prisma = new PrismaClient ();
// Sequential 트랜잭션은 순차적으로 실행됩니다.
// 결과값은 각 쿼리의 순서대로 배열에 담겨 반환됩니다.
const [ posts , comments ] = await prisma . $transaction ([
prisma . posts . findMany (),
prisma . comments . findMany (),
]);
- Sequential 트랜잭션 2
import { PrismaClient } from '@prisma/client' ;
const prisma = new PrismaClient ();
// Sequential 트랜잭션은 순차적으로 실행됩니다.
// Raw Quyery를 이용하여, 트랜잭션을 실행할 수 있습니다.
const [ users , userInfos ] = await prisma . $transaction ([
prisma . $queryRaw `SELECT * FROM Users` ,
prisma . $queryRaw `SELECT * FROM UserInfos` ,
]);
Sequentai 트랜잭션 은 Prisma의 여러 쿼리를 배열([]) 로 전달받아,
각 쿼리들을 순서대로 실행하는 특징 이 있습니다. 이러한 특징은
여러 작업이 순차적으로 실행 되어야 할 때 사용할 수 있습니다.
3. 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 인스턴스 와 같은 기능을 수행합니다.
4. 회원가입 API 트랜잭션 적용하기
사용자 및 사용자 정보 를 생성하는 과정에서 에러가 발생할 수 있 기 때문에,
트랜잭션(Transaction) 을 도입해 하나의 작업으로 묶어주겠습니다.
먼저,
import { Prisma } from "@prisma/client" ;
을 import 받은 후,
// User 테이블에 값 넣기
const user = await prisma . Users . create ({
data : {
email ,
password : hashedPassword ,
},
});
// UserInfos 테이블에 값 넣기
const userInfo = await prisma . UserInfos . create ({
data : {
userId : user . userId ,
//생성한 유저의 userId를 바탕으로 사용자 정보를 생성합니다.
name ,
age ,
gender : gender . toUpperCase (), //성별 대문자로 변환
profileImage ,
},
});
기존 작업 물을 복사한 뒤
const [ 첫번째 테이블 결과 , 두번째 테이블 결과 ] = await prisma . $transaction (
async tx => {
//기존 작업 물 붙여 넣기
// 콜백 함수의 리턴값으로 사용자와 사용자 정보를 반환합니다.
return [ 첫 번째 테이블 결과 , 두 번째 테이블 결과 ];
},
{
isolationLevel : Prisma . TransactionIsolationLevel . ReadCommitted ,
}
);
위와 같은 틀에 붙여넣습니다.
그 후, 우리는 transaction을 사용해야하기 때문에,
prisma의 각 테이블 조회 부분을
const user = await prisma . Users . create ({
const userInfo = await prisma . UserInfos . create ({
const user = await tx . Users . create ({
const userInfo = await tx . UserInfos . create ({
으로 수정해주고,
return [ 첫 번째 테이블 결과 , 두 번째 테이블 결과 ];
을
으로 테이블 조회 결과를 배열 분해 및 할당으로 전달합니다.
전체 코드 :
const [ user , userInfo ] = await prisma . $transaction (
async tx => {
// User 테이블에 값 넣기
const user = await tx . Users . create ({
data : {
email ,
password : hashedPassword ,
},
});
// UserInfos 테이블에 값 넣기
const userInfo = await tx . UserInfos . create ({
data : {
userId : user . userId ,
//생성한 유저의 userId를 바탕으로 사용자 정보를 생성합니다.
name ,
age ,
gender : gender . toUpperCase (), //성별 대문자로 변환
profileImage ,
},
});
return [ user , userInfo ];
},
{
isolationLevel : Prisma . TransactionIsolationLevel . ReadCommitted ,
}
);
위의 코드를 분석해보면,
await prisma . $transaction (
prisma.$transaction으로 트랜직션을 사용할 것을 명시적으로 작성합니다.
그리고,
const user = await tx . Users . create ({
const userInfo = await tx . UserInfos . create ({
해당 tx(transaction) 값으로 해당 쿼리 내부를 실행시킵니다.
마지막으로, 최종 로직이 완료 됐을 때
결과를 반환되고,
const [ user , userInfo ] = await prisma . $transaction (
에 할당 됩니다.
{
isolationLevel : Prisma . TransactionIsolationLevel . ReadCommitted ,
}
은 격리수준으로
키를 바탕으로,
Prisma . TransactionIsolationLevel . ReadCommitted ,
Prisma의 TransactionIsolationLevel을 ReadCommitted으로 지정해줍니다.
TranactionIsolationLevel에는
ReadUncommitted : 트랜잭션이 아직 커밋되지 않은 데이터도 읽을 수 있다.
ReadCommitted : 실제 커밋된 결과값만 읽을 수 있도록 한다.
RepeatableRead : 트랜잭션이 시작된 후 데이터를 다시 읽을 때 동일한 결과가 보장됩니다.
Serializeable : 가장 높은 격리 수준으로, 트랜잭션이 직렬화된 것 처럼 실행됩니다. 모든 종류의 일관성 문제를 방지하지만 성능에 큰 영향을 줄 수 있습니다.
이 존재합니다.
이제 Insomnia에서 회원가입을 진행하면
위와 같이 정상적으로 회원가입이 완료되고,
log.middleware.js 를 통해
Transaction의ISOLATION LEVEL이 READ COMMITTED 이라는 걸 확인할 수 있습니다.
만약 고의적으로 Error를 발생시킬 경우,
트랜잭션이 ROLLBACK되는걸 확인할 수 있습니다.
5. 사용자 히스토리 모델 생성하기
사용자 히스토리(UsersHistories) 테이블이 생성되었습니다.
이 테이블은 사용자의 정보가 변경될 때 마다 변경 내역을 로깅(Logging) 하기 위해 사용 됩니다.
사용자 정보 변경 API를 구현하면서, 이 변경 내역을 사용자 히스토리 테이블에도 함께 데이터를
생성하도록 구현하겠습니다.
model UserHistories{ userHistoryId String @id @default(uuid()) @map("userHistoryId")
//uuid()는 실제 생성된 데이터의 날짜나 시간 등 다양한 정보를 제공하는 식별자입니다. userId Int @map("userId") changedField String @map("changedField") oldValue String? @map("oldValue") newValue String @map("newValue") changedAt DateTime @updatedAt @map("changedAt") // Users 테이블과 관계를 설정합니다. user Users @relation(fields: [userId], references: [userId], onDelete : Cascade) @@map("UserHistories") }
내용을 schema.prisma에 추가해줍니다.
그리고,
model Users {
~~~~
userHistories UserHistories[] // 사용자(Users) 테이블과 사용자 히스토리(UserHistories) 테이블이 1 : N 관계를 맺습니다. }
을 추가시켜줍니다.
위와 같이 수정 및 작성을 완료한 후,
npx prisma db push를 수행해줍니다.
UUID(범용 고유 식별자)란 무엇인가요?
총 4가지 정보 를 하이픈 (-)으로 구분하여 순차적으로 저장한 데이터 타입 입니다.
시간 정보를 포함 하고 있어 새성된 순서대로 정렬 이 되는 특징을 가지고 있습니다.
사용자 히스토리 테이블은 UUID 를 사용하여 컬럼 수를 최소화
하는 것이 로그 테이블에서는 더욱 효율적인 설계가 될 것입니다
6. 사용자 정보 변경 API
// 사용자 정보 변경 API
UsersRouter . 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 ,
},
});
if ( ! userInfo )
return res
. status ( 404 )
. json ({ mssage : "사용자 정보가 존재하지 않습니다." });
await prisma . $transaction (
async tx => {
await tx . UserInfos . update ({
where : {
userId : + userId ,
},
data : {
... updatedData ,
//spread 연산자로 변경사항이 있는 값만 변경해준다.
},
});
//반복문과 조건문으로 변경이 있는 데이터만 변경을 조회한다.
for ( let key in updatedData ) {
if ( userInfo [ key ] !== updatedData [ key ]) {
await tx . UserHistories . create ({
data : {
userId : + userId ,
changedField : key ,
// key가 변경된 값이기 때문에 key를 제공한다.
oldValue : String ( userInfo [ key ]),
// 이전 값은 userInfo에 있으니깐 userInfo[key]로 값을 전달
newValue : String ( updatedData [ key ]),
// 새로운값은 updatedData가 갖고 있어 때문에 updatedDaya[key]로 값을 전달
},
});
}
}
},
{
isolationLevel : Prisma . TransactionIsolationLevel . ReadCommitted ,
}
);
return res
. status ( 200 )
. json ({ massage : "사용자 정보 변경에 성공하였습니다." });
} catch ( err ) {
next ( err );
}
});
Insomnia에서 변경을 시켜보면,
변경전
변경 후
변경된 결과 확인
History Table 변경 정보
위와 같이 변경이 잘 이뤄지고, History Table에도 잘 반영된 것을 확인할 수 있습니다.