다마고치 백엔드 프로젝트 진행중 회원가입시 user 테이블에 회원 정보를 저장과 동시에
다른 여러가지 쿼리 작업을 해야하는 상황이 생겼다
상황
try {
const message = await userService.joinUser(uid, hashPwd, nickName);
const user = await userService.getUser(uid, hashPwd);
const pet = await petService.getPet();
await userService.setUserPet(user.id, pet.id); //회원가입 시 펫 증정
const food = await foodService.getFood(1);
await userService.setUserfood(user.id, food.id); //기본 먹이 증정 로직
res.status(201).json(message);
} catch (e) {
next(e);
}
회원가입시 진행해야되는 절차는 위의 코드처럼
- db에 유저 데이터 저장
- 유저의 고유한 id값을 얻기 위해 저장된 유저 데이터 요청
- 기본 펫 증정을 위해 펫 데이터 요청
- 펫 데이터를 통해 유저의 펫 데이터 저장
- 기본 먹이도 줘야하기 때문에 먹이 요청
- 먹이 데이터를 통해 유저의 먹이 데이터 저장
하지만 이런식으로 하면 중간에 하나라도 에러가 발생하면 안된다
그렇게 되면 데이터의 결점이 생기게 된다
그래서 위의 로직을 하나의 트랜젝션으로 진행하기 위해 mariadb 공식 문서를 찾아보았다
트랜젝션 시도

위 사진에서 보다시피 2중 try, catch 문법과 하나의 conn(connection) 을 파라미터로 받아서 트랜젝션을 진행하고
문제없이 쿼리가 완료되면 commit, 에러가 발생하면 rollback을 진행한다 해당 문서를 참고하여 나도 코드를 짜보았다
//트랜젝션 함수
joinTx: async (uid: string, hashPwd: string, nickName: string) => {
const conn = await fetchConn();
try {
// Start Transaction
await conn.beginTransaction();
try {
// Add Data in a batch
await conn.batch(
'INSERT INTO users (uid, pwd, nick_name) VALUES (?, ?, ?)', // 회원 등록
[uid, hashPwd, nickName]
);
const user = await conn.query(
'SELECT id, nick_name FROM users WHERE uid = ? AND pwd = ?', // 회원 조회
[uid, hashPwd]
);
const pet = await conn.query('SELECT * FROM pets WHERE id = ?', [1]); //팻 조회
await conn.batch(
'INSERT INTO user_current_pet (user_id, pet_id) VALUES (?, ?)', //유저 펫 셋팅
[user[0].id, pet[0].id]
);
const food = await conn.query(
'SELECT * FROM foods WHERE id = ?', //먹이 조회
[1]
);
await conn.batch(
'INSERT INTO user_foods (user_id, food_id) VALUES (? , ?)', //유저와 먹이 셋팅
[1] //해당 시점에서 실패
);
//--------
// Commit Changes
await conn.commit();
} catch (e) {
console.error('err', e);
await conn.rollback();
throw e;
}
} catch (e) {
console.error('err ', e);
throw e;
}

테스트아이디 6번으로 회원가입 시도

트랜젝션 실패 후 users 테이블 확인해보면 테스트 아이디 6번은 INSERT되지 않은걸 볼 수 있다

공식 문서에서 가르쳐준 대로 트랜젝션을 진행하였는데 잘 동작하였다 하지만 내 코드는 너무 지저분하고 유지보수가 힘들것이다 그래서 트랜젝션 코드들을 어떻게 관리해야할지 고민했다
유지보수 측면에서 관리방법..?
//기존에 사용하던 단일 쿼리
const sqlTemplate = {
getQuery: async (sql: string | QueryOptions, ...values: unknown[]) => {
let conn;
try {
conn = await fetchConn();
const rows = await conn.query(sql, values);
if (!rows[0]) throw new Error('일치하는 정보가 없음');
return rows;
} catch (e) {
console.log(e);
throw e;
} finally {
if (conn) conn.end();
}
},
//sqlTemplate.getQuery('SELECT id, nick_name FROM users WHERE uid = ? AND pwd = ?', uid, hashPwd)
service 로직에서 사용했던 단일 쿼리이다 코드를 보면 db작업이 끝나고 db와의 연결을 종료한다
그렇기 때문에 트랜젝션 코드에서 해당 템플릿을 사용하지 못한다
그래서 위의 sql템플릿과 같이 트랜잭션 템플릿을 만들고 재사용하려고 했었는데
만약 테이블에 컬럼이 추가되면 기존 템플릿과 트랜젝션 템플릿 2곳의 쿼리문을 수정해야 한다

나는 db 테이블이 수정되면 sql 쿼리문도 한번만 수정되게 관리하고 싶어서
내가 생각한 최선의 방법은 sql 쿼리문 만 따로 관리하는 객체를 만들어 관리하는 방법이다

해당 방법보다 더 효율적인 방법을 찾는다면 개선할 예정이다
더 효율적인 방법!
위처럼 트랜젝션 로직을 관리하는 방법에 대해 데브코스 멘토님에게 의견을 물어보았는데
나는 생각지도 못한 방법을 제시해 주셨다
class SqlTemplate {
async modifyQuery(uid: string, hasPwd: string, nickName: string, conn?: Conn) {
//트랜젝션 진행시에는 conn을 인자로 넘겨줌 conn 존재 여부에 따라 핸들링 가능
const connection = conn ?? await fetchConn();
await connection.query(`UPDATE ...`, [uid, hasPwd, nickName] );
// ...
}
}
class TransactionService {
async transaction<T>(context: (tx: Conn) => Promise<T>) {
const conn = await fetchConn();
try {
await conn.beginTransaction();
await context(conn); // conn을 인자로 받는 콜백함수
await conn.commitTransaction();
return result;
} catch (err) {
await conn.rollbackTransaction();
throw err;
} finally {
conn.release();
}
}
}
class UserService {
constructor(transactionService: TransactionService, sqlTemplate: SqlTemplate)
async joinUser(uid: string, hasPwd: string, nickName: string) {
await this.transactionService.transaction(async (conn) => { //트랜젝션 로직을 콜백함수로 전달
await this.sqlTemplate.modifyQuery(uid, hasPwd, nickName, conn)
})
}
}
sql 템플릿을 두개를 관리하는게 아니라 기존 sql 템플릿에 conn 파라미터를 옵션으로 받으면 conn의 존재 여부에 따라 핸들링이 가능하고 트랜젝션 클래스에서는 context라는 콜백함수를 받아서 conn을 인자로 넘겨주는 방법이다
class UserService {
TxnService;
SqlTemplate;
constructor() {
this.TxnService = new TxnService();
this.SqlTemplate = new SqlTemplate();
}
async joinUser(uid: string, hashPwd: string, nickName: string) {
await this.TxnService.transaction(async conn => {
await this.setUser(uid, hashPwd, nickName, conn);
const user = await this.getUser(uid, hashPwd, conn);
const pet = await new PetService().getRandomPet(conn);
const food = await new FoodService().getFood(1, conn);
await this.setPetForUser(user.id, pet.id, conn);
await this.setFoodForUser(user.id, food.id, conn);
});
}
피드백을 참고하여 개선된 나의 회원가입 트랜젝션
조금 더 개선
트랜젝션 서비스 클래스에 있는 트랜젝션 메서드를 sql템플릿 클래스의 메서드로 옮겼다
class SqlTemplate {
async getQuery(sql: string | QueryOptions, values?: unknown[], conn?: PoolConnection) {
//...
}
async modifyQuery(sql: string | QueryOptions, values?: unknown[], conn?: PoolConnection) {
//...
}
async transaction<T>(context: (conn: PoolConnection) => Promise<T>) {
let connection;
try {
connection = await fetchConn();
await connection.beginTransaction();
try {
await context(connection);
await connection.commit();
} catch (e) {
await connection.rollback();
throw e;
}
} catch (e) {
throw e;
} finally {
if (connection) connection.end();
}
}
}
이렇게 sql템플릿에서 트랜젝션 메서드를 작성하면 트랜젝션 서비스 클래스는 필요없어지므로
다른 서비스 클래스들이 가지고 있는 필드를 하나 줄일수 있다
class UserService {
private SqlTemplate;
constructor() {
this.SqlTemplate = new SqlTemplate();//하나만 있으면 OK
}
async joinUser(uid: string, hashPwd: string, nickName: string) {
await this.SqlTemplate.transaction(async conn => {
await this.setUser(uid, hashPwd, nickName, conn);
const user = await this.getUser(uid, hashPwd, conn);
const pet = await petService.getRandomPet(conn);
const food = await foodService.getFood(1, conn);
await this.setPetForUser(user.id, pet.id, conn);
await this.setFoodForUser(user.id, food.id, conn);
await requestService.createRequestTime(user.id, conn);
});
}
}
mariadb 공식 문서
https://mariadb.com/docs/server/connect/programming-languages/nodejs/promise/transactions/
Open Source Database (RDBMS) for the Enterprise | MariaDB
MariaDB is the leading enterprise open source database with features previously only available in costly proprietary databases. Enterprise grade, wallet friendly.
mariadb.com
'database' 카테고리의 다른 글
| 테이블 2개 JOIN시 COUNT값이 곱셈이 된다? (0) | 2024.08.17 |
|---|---|
| 시퀄라이즈 모델들 비동기적으로 연결하기 (0) | 2024.04.13 |
| ORM 사용 후기 (0) | 2024.04.11 |
| MySQL 자료형 (1) | 2024.03.14 |
| SQL 문법(DDL, DML, DCL, DQL,TCL) (0) | 2024.03.01 |