๋ฌธ์
NestJS์์ TypeORM์ ์ฌ์ฉํ์ฌ ํธ๋์ญ์
์ ๊ตฌํํ ๋, ํ๋์ ๋ฉ์๋๊ฐ ๋๋ฌด ๋ง์ ์ฑ
์์ ๊ฐ์ง๊ณ ์๋ค.
์ค๋ณต๋ ์ฝ๋๊ฐ ์กด์ฌํ๋ค.
์ฝ๋
Copy async updateInfo(
data: EditUserInfoData,
): Promise<UserInfoResponse> {
// QueryRunner Connect
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
// Transaction Start
await queryRunner.startTransaction();
try {
// ์ค์ ๋ก์ง
const userAccount = await queryRunner.manager.findOneBy(UserAccount, {
id: id,
deleted: false,
});
if (!userAccount) {
throw new UserAccountNotFoundError();
}
userAccount.nickname = data.nickname;
userAccount.address = data.address;
userAccount.introduction = data.introduction
? data.introduction
: userAccount.introduction;
userAccount.updatedAt = new Date();
await queryRunner.manager.save(UserAccount, userAccount);
return {
nickname: userAccount.nickname.value,
address: userAccount.address.value,
introduction: userAccount.introduction?.value,
};
} catch (err) {
// Rollback
await queryRunner.rollbackTransaction();
} finally {
// Release
await queryRunner.release()
}
}
์ ์ฝ๋์์ try {}
๋ฌธ ์ธ์ ์ฝ๋ ์ค๋ณต๋๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ
TypeScript์ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ฌ์ฉํ์ฌ ๊ณตํต ๋ถ๋ถ์ ๋ถ๋ฆฌํ๋ ๊ฒ์ด ๊ฐ๋ฅํ ๊ฒ์ผ๋ก ๋ณด์ธ๋ค.
ํด๊ฒฐ ๊ณผ์
Step1
@Transactional()
Decorator๋ฅผ ๊ตฌํํด์ ๊ณตํต ๋ก์ง์ Decorator์์ ๊ตฌํํ๋๋ก ํ๋ค.
Copy @Transactional()
async updateInfo(data: EditUserInfoData) {
const userAccount = await this.findById(data.userId);
userAccount.nickname = data.nickname;
userAccount.address = data.address;
const now = new Date();
userAccount.updatedAt = now;
return this.userAccountRepository.save(userAccount, { transaction: false });
}
๊ณตํต ๋ก์ง(Transactional Decorator)
Copy export function Transactional(
isolationLevel?: IsolationLevel,
): MethodDecorator {
return (
_target: any,
_propertyKey: string,
descriptor: PropertyDescriptor,
) => {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const dataSource: DataSource = this.dataSource;
if (!dataSource) {
throw new Error('DataSource is not injected');
}
// ํธ๋์ญ์
์ ์ด๋ฏธ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ์ถ๊ฐ๋ก ํธ๋์ญ์
์ ์์ฑํ์ง ์๋๋ค.
const existsTransaction = args.find(
(arg) => arg.connection !== undefined,
);
if (existsTransaction) {
return originalMethod.apply(this, args);
}
// ํธ๋์ญ์
์ ์ฌ์ฉํ์ง ์๋ ๊ฒฝ์ฐ ํธ๋์ญ์
์ ์์ฑํ๋ค.
const queryRunner: QueryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction(
isolationLevel ? isolationLevel : DEFAULT_ISOLATION_LEVEL,
);
try {
args = args.concat(queryRunner);
const result = await originalMethod.apply(this, args);
await queryRunner.commitTransaction();
return result;
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
};
};
}
Step1 ๋ฌธ์ ์
๋น์ฆ๋์ค ๋ก์ง์ Connect์ ๊ณตํต ๋ก์ง์ Connect๊ฐ ์๋ก ๋ค๋ฅด๊ธฐ ๋๋ฌธ์, ๋น์ฆ๋์ค ๋ก์ง์ ํธ๋์ญ์
์ผ๋ก ๊ฐ์ธ๋ ๊ฒ์ด ๋ชฉํ์์ผ๋ ์ด๋ฅผ ๋ฌ์ฑํ์ง ๋ชปํ๋ค.
๊ณตํต ๋ก์ง์ Connect 47
๋น์ง๋์ค ๋ก์ง์ Conenct 48
Step2
๋น์ฆ๋์ค ๋ก์ง์์ QueryRunner๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌํํ๊ณ , ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ํตํด ์ด๋ฏธ ํธ๋์ญ์
์ด ์ฌ์ฉ ์ค์ธ์ง ํ์ธํ๊ณ , ํ์์ ๋ฐ๋ผ ์ถ๊ฐ์ ์ผ๋ก ํธ๋์ญ์
์ ์์ํ๊ฑฐ๋ QueryRunner๋ฅผ ์ฃผ์
ํ๋ค.
Copy @Transactional()
async updateInfo(data: EditUserInfoData, queryRunner?: QueryRunner) {
const userAccount = await this.findByIdWithQueryRunner(
data.userId,
queryRunner,
);
userAccount.nickname = data.nickname;
userAccount.address = data.address;
userAccount.introduction = data.introduction
? data.introduction
: userAccount.introduction;
const now = new Date();
userAccount.updatedAt = now;
await this.saveWithQueryRunner(userAccount, queryRunner);
return {
nickname: userAccount.nickname.value,
address: userAccount.address.value,
introduction: userAccount.introduction?.value,
};
}
async findByIdWithQueryRunner(
id: ObjectId,
queryRunner: QueryRunner,
): Promise<UserAccount> {
const userAccount = await queryRunner.manager.findOneBy(UserAccount, {
id: id,
});
if (!userAccount) {
throw new UserAccountNotFoundError();
}
return userAccount;
}
async saveWithQueryRunner(
userAccount: UserAccount,
queryRunner: QueryRunner,
) {
return queryRunner.manager.save(UserAccount, userAccount);
}
๊ณตํต ๋ก์ง(Transactional Decorator)
Copy export function Transactional(
isolationLevel?: IsolationLevel,
): MethodDecorator {
return (
_target: any,
_propertyKey: string,
descriptor: PropertyDescriptor,
) => {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const dataSource: DataSource = this.dataSource;
if (!dataSource) {
throw new Error('DataSource is not injected');
}
// ํธ๋์ญ์
์ ์ด๋ฏธ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ์ถ๊ฐ๋ก ํธ๋์ญ์
์ ์์ฑํ์ง ์๋๋ค.
const existsTransaction = args.find(
(arg) => arg.connection !== undefined,
);
if (existsTransaction) {
return originalMethod.apply(this, args);
}
// ํธ๋์ญ์
์ ์ฌ์ฉํ์ง ์๋ ๊ฒฝ์ฐ ํธ๋์ญ์
์ ์์ฑํ๋ค.
const queryRunner: QueryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction(
isolationLevel ? isolationLevel : DEFAULT_ISOLATION_LEVEL,
);
try {
args = args.concat(queryRunner);
const result = await originalMethod.apply(this, args);
await queryRunner.commitTransaction();
return result;
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
};
};
}
ํ๋์ connect ์์ ํ๋์ transactional ์์์ ๋ชจ๋ ๋ก์ง์ด ์คํํ๋ค.
๊ฒฐ๋ก
NestJS์ TypeORM์ ์ฌ์ฉํ์ฌ ํธ๋์ญ์
๊ด๋ฆฌ๋ฅผ ์ํ ์ฝ๋ ์ค๋ณต์ ์ค์ด๊ณ , ๋ฉ์๋์ ์ฑ
์์ ๋ถ์ฐํ๋ ๋ฐฉ๋ฒ์ ํ๊ตฌํ๋ค.
Transactional Decorator์ ๋์
๋น์ฆ๋์ค ๋ก์ง๊ณผ ํธ๋์ญ์
๊ด๋ฆฌ์ ๋ถ๋ฆฌ
ํ๋์ Connect ๋ด์์์ ํธ๋์ญ์
์คํ
๊ฒฐ๋ก ์ ์ผ๋กTransactional
๋ฐ์ฝ๋ ์ดํฐ์ ์ฌ์ฉ๊ณผ QueryRunner
์ ์ ์ ํ ๊ด๋ฆฌ๋ฅผ ํตํด, NestJS ๋ฐ TypeORM ํ๊ฒฝ์์ ํธ๋์ญ์
์ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์๋ ๊ตฌ์กฐ๋ฅผ ๊ตฌ์ถํ๋ค. ํธ๋์ญ์
๊ด๋ฆฌ์ ๋ํ ์ค๋ณต ์ฝ๋๋ฅผ ์ค์ด๊ณ , ๋น์ฆ๋์ค ๋ก์ง์ ๋ช
ํ์ฑ ๋ฐ ์ ์ง๋ณด์์ฑ์ ํฅ์์์ผฐ๋ค.
๋ฌผ๋ก ,์ด ๋ฐ์ฝ๋ ์ดํฐ ์ฌ์ฉํ๋ ๊ฒ์ด ์ด๋ค ์ถ๊ฐ์ ์ธ ์ํฅ์ ๋ฏธ์น ์ง๋ ์์ง ๋ถํ์คํ๋ค. ํ
์คํธ์ฝ๋๋ฅผ ์์ฑํ๊ณ ์ง์์ ์ผ๋ก ํ์ธํด์ ๊ฐ์ ์์
ํ ์์ ์ด๋ค.