ํธ๋์ญ์ ์ฒ๋ฆฌํ๊ธฐ
ํ ์คํธ๊ณผ์ ์์ ๋๋์ฒด ์ ์ด๋ด๊น๋ฅผ ํ๋ฃจ์ข ์ผ ๊ณ ๋ฏผํ ํ๋ก์ ํธ์ด๋ค.
ํ์คํ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ ๋ ๊ตฌํ์ด ๋น ๋ฅด๋ค๋ ์ฅ์ ์ด ์์ง๋ง, ํ ์คํธ์์ ์ด๋ค๊ฒ ๋ฌธ์ ์ธ์ง ํ์ธํ๊ธฐ ์ด๋ ต๋ค๋ ๋จ์ ๋ ์์๋ค.
๊ธฐ์กด ์ฟผ๋ฆฌ ์ต์ ํ๋ฅผ ๋ณด๊ณ ์ค๋ ๊ฒ๋ ์ถ์ฒํ๋ค.
์ด๊ฑธ ํ๊ณ ๋์ ๊ทธ ๋ค์์ ์ฒ๋ฆฌํ ๊ณผ์ ์ด๊ธฐ ๋๋ฌธ์ด๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ ๋ ํธ๋์ญ์ ์ด๋ ๋ฌด์์ผ๊น?
์ ๋ง ๊ฐ๋จํ๊ฒ, ํธ๋์ญ์ ์ DB์ ๋ณด๋ด๋ ์ฟผ๋ฆฌ๋ฅผ ๋ฌถ์์ด๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค.
์ฟผ๋ฆฌ๋ฅผ ํ๋์ฉ ๊ณ์ ๋ณด๋ด๋ ๊ฒ์ด ์๋, ํ๋๋ฅผ ํต์งธ๋ก ๋ณด๋ด์ ์ฑ๊ณตํ๋ฉด ๋ณ๊ฒฝ ์ฌํญ์ ์ ๋ถ ์ ์ฉ์ํค๊ณ (์ปค๋ฐ), ์คํจํ๋ฉด ํด๋น ์ฟผ๋ฆฌ๋ค์ ์ํ ๋ณ๊ฒฝ์ ์ด๊ธฐํ์ํค๋(๋กค๋ฐฑ) ๋ฌถ์์ด๋ค.
์ด๋ฅผ ์ฐ๋ฆฌ ํ๋ก์ ํธ์์ ์ ์ฉ์์ผ์ผ ํ ์ด์ ๊ฐ ์์๋ค.
๋ฌธ์ ์ง์ ๋ณต์ฌํ ๋, ๋ณต์ฌํ ์๋ณธ ๋ฌธ์ ์ง์ copyCount๋ฅผ 1 ์ฆ๊ฐ์ํค๊ณ ,
๋ณต์ฌ๋ ์ง๋ฌธ๋ค์ ์๋ก์ด copy๋ Question์ผ๋ก ์ ์ฅ์ํจ๋ค
์ด๋ฐ ๋ก์ง์ ๋ง๋ค์ด์ ์ฐ๋ ๊ณผ์ ์์, ๋ฌธ์ ์ง ์๋ณธ์ ์ฐพ์์ copyCount๋ฅผ 1 ์ฆ๊ฐ์ํจ ํ, Question์ ๋ณต์ฌํ๋ ค๊ณ ํ๋ค.
๊ทผ๋ฐ ๋ณต์ฌํ๋ ค๋ ์ง๋ฌธ์ด ์์ ๋ ์๋ฌ์ฒ๋ฆฌ๊ฐ ๋๊ณ , 400์๋ฌ๋ฅผ ๋ฐํํ๋ค.
์ด๊ฑธ ์ฒ์๋ถํฐ ๋ชจ๋ ๊ฑธ ์กฐํํ๊ณ ๊ฒ์ฆํ ํ, copyCount๋ฅผ ์ฆ๊ฐ์ํค๋ฉด ๊ด์ฐฎ๊ฒ ์ง๋ง, ์ด๊ฑธ ํธ๋์ญ์ ํ ์์ผ์ ์คํจ์ ๊ทธ๋ฅ copyCount์ ์ฆ๊ฐ๋ฅผ ๋กค๋ฐฑ์์ผ ๋ฒ๋ฆฌ๋ ๊ณผ์ ์ ์ถ๊ฐํด๋ณด๋ ๊ฒ๋ ์ข์ ํ์ต์ผ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ๋ค.
๋น์ฆ๋์ค ๋ก์ง์์ ์ด๋ฅผ ์ ์ฉ์ํค๋ ค๋ฉด ์ด๋ป๊ฒ ํด์ผํ ๊น?
nestjs&typeorm์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ๋ QueryRunner๋ฅผ ์ด์ฉํด ์์์ ๋งํ ๋น์ฆ๋์ค ๋ก์ง์ ์ฒ๋ฆฌํด๋ณด์๋ค.
@Injectable()
export class QuestionService {
private queryRunner: QueryRunner;
constructor(
private questionRepository: QuestionRepository,
private workbookRepository: WorkbookRepository,
dataSource: DataSource,
) {
this.queryRunner = dataSource.createQueryRunner();
}
async copyQuestions(
copyQuestionRequest: CopyQuestionRequest,
member: Member,
) {
await this.queryRunner.connect();
await this.queryRunner.startTransaction();
try {
const workbook = await this.workbookRepository.findById(
copyQuestionRequest.workbookId,
);
validateWorkbook(workbook);
validateWorkbookOwner(workbook, member);
const questions = await this.questionRepository.findAllByIds(
copyQuestionRequest.questionIds,
);
Array.from(
new Set(questions.map((question) => question.workbook)),
).forEach(async (workbook) => {
workbook.increaseCopyCount();
await this.workbookRepository.update(workbook);
});
await this.questionRepository.saveAll(
questions.map((question) => this.createCopy(question, workbook)),
);
await this.queryRunner.commitTransaction();
return WorkbookIdResponse.of(workbook);
} catch (e) {
await this.queryRunner.rollbackTransaction();
} finally {
await this.queryRunner.release();
}
}
}
์ด๋ฐ์์ผ๋ก QueryRunner๋ฅผ ์ฐ๊ฒฐ์์ผ์ค ํ, ํธ๋์ญ์ ์ ์์ํ๋ค.
return ์ ์ ํธ๋์ญ์ ์ ์ปค๋ฐํด์ฃผ๊ณ returnํ๋ฉฐ, ์์ธ์ฒ๋ฆฌ๋ก catch๋ก ๋์ด๊ฐ๋ฉด ๋กค๋ฐฑ์ ์์ผ์ค๋ค.
๋ฌด์์ด ๋์๊ฑด, connect์ํจ ์ฟผ๋ฆฌ ๋ฌ๋๋ฅผ ๋ฆด๋ฆฌ์ฆํด์ค๋ค.
์ด๋ ๊ฒ ํด์ ํธ๋์ญ์ ํ๋ฅผ ์์ผ์ค ์ ์๋ค๋ ๋ฌธ์๋ค์ gitbook, TypeOrm์์ ๋ณด์๋ค.
ํ์ง๋ง ์ ์ผ ํฐ ๋ฌธ์ ๋, ๋น์ฆ๋์ค ๋ก์ง์ด ๋๋ฌด ๋๋ฌ์์ง ๊ฒ ๊ฐ๋ค๋ ์ ์ด๋ค.
๋งค ์๊ฐ๋ง๋ค ์ ๊ฑธ ๋ฉ์๋ ์์ ๋ฃ์ด์ฃผ์ด์ผ ํ๋ฉฐ, try-catch-finally๋ฅผ ๋ฉ์๋ํ ์ํค๊ณ , ์์ ์ฝ๋ฐฑ ๋ฉ์๋๋ฅผ ๋ฃ๊ณ , ๊ทธ ์์ return ์ ์ commit์ ์ํจ๋ค๋ ๊ฒ์ด ๋๋ฌด ๊ท์ฐฎ์๋ค.
๊ท์ฐจ๋์ฆ์ ํด๊ฒฐํ๊ธฐ ์ํ ๋ฐฉ๋ฒ์ ์ฐพ์๋ณด์๋ค.
์ด๊ฑธ ์ฑํํ ๊ฐ์ฅ ํฐ ์ด์ ๋, JPA์ฒ๋ผ @Transactional๋ก ํ๋์ ํธ๋์ญ์ ์ฒ๋ฆฌ๋ฅผ ํธํ๊ฒ ํ๊ณ ์ถ์๊ธฐ ๋๋ฌธ์ด๋ค.
์ด๋ ๊ฒ ์ด๋ ธํ ์ด์ (์ ์ด ์ธ๊ณ์์ ๋ฐ์ฝ๋ ์ดํฐ์๋๊ฐ...)๋ฅผ ์ฌ์ฉํด์ ํธํ๊ฒ ๊ฐ๋ฐํ ์ ์์๊น๋ผ๋ ์๊ฐ์ด ๋ค์๋ค.
์ด ๋ typeorm-transactional๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ณด๊ฒ ๋์๋ค.
์ด๊ฑธ ์ด์ฉํด์ ๊ทนํ์ ์ด๋์ถฉ์ด ๋์ด๋ณด๊ธฐ๋ก ํ๋ค.
๊ณต์๋ฌธ์๋๋ก ํ๋์ฉ ์ ์ฉํด๋ณด์.
// main.ts
async function bootstrap() {
initializeTransactionalContext();
const app = await NestFactory.create(AppModule);
...
// app.module.ts
@Module({
imports: [
TypeOrmModule.forRootAsync(MYSQL_OPTION),
....
// MYSQL_OPTION
export const MYSQL_OPTION: TypeOrmModuleAsyncOptions = {
useFactory() {
return {
type: 'mysql',
name: 'main',
host: process.env.DATABASE_HOST,
port: Number(process.env.DATABASE_PORT),
entities: [Member, Category, Workbook, Question, Answer, Video],
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE,
autoLoadEntities: true,
synchronize: true,
};
},
async dataSourceFactory(options) {
if (!options) {
throw new Error('Invalid options passed');
}
return addTransactionalDataSource(new DataSource(options));
},
};
์ด์ TypeOrm์ ๋น๋๊ธฐ ๋ชจ๋๋ก ๋ฑ๋กํด์ฃผ๊ณ , ์ด ๋ dataSourceFactory๋ฅผ ๋ง๋ค์ด์ค๋ค.
์ฌ๊ธฐ์์ addTransactionalDataSource๋ฅผ ํตํด์ TransactionalContext๋ฅผ ๋ง๋ค์ด์ค๋ค.
์ด๋ ํธ๋์ญ์ ์ฒ๋ฆฌ๋ฅผ ์ํ ๋ค๋ฆฌ๋ฅผ ๋ง๋ค์ด์ฃผ๋ ๋ก์ง์ด๋ค.
์ฐ๋ฆฌ๋ ํ๋ก์ ํธ ์์์์ ํ๋์ ํธ๋์ญ์ ์ปจํ ์คํธ๋ฅผ ๋ง๋ค๊ณ , ์ฌ๊ธฐ์์๋ง ํธ๋์ญ์ ์ ์ฒ๋ฆฌํด์ฃผ๊ธฐ ์ํด
ํ๋ก์ ํธ ์คํ์ ํ๋์ ํธ๋์ญ์ ์ปจํ ์คํธ๋ฅผ ๋ง๋ค๊ฒ ํ๋ค.
@Transactional()
async createWorkbook(
createWorkbookRequest: CreateWorkbookRequest,
member: Member,
) {
const category = await this.categoryRepository.findByCategoryId(
createWorkbookRequest.categoryId,
);
validateManipulatedToken(member);
validateCategory(category);
const workbook = Workbook.of(
createWorkbookRequest.title,
createWorkbookRequest.content,
category,
member,
createWorkbookRequest.isPublic,
);
const result = await this.workbookRepository.insert(workbook);
return result.identifiers[0].id as number;
}
์ด์ ์ด๋ฐ ์์ผ๋ก @Transactional()๋ฐ์ฝ๋ ์ดํฐ ํ๋๋ฅผ ๋ฃ์ด์ฃผ๋ฉด ๋๋ค.
์ด๋ฐ์์ผ๋ก ์์ฒญ์ ๋ณด๋ผ ๋,
์ด๋ฐ์์ผ๋ก ํ๋์ ํธ๋์ญ์ ํ๋ฅผ ์ํค๋ ์ฑ๊ณต์ ์ธ ๊ฒฐ๊ณผ๊ฐ ๋์๋ค.
๋ผ๊ณ ์๊ฐํ๋ค.
๊ทผ๋ฐ ์๋นํ ๋ฌธ์ ๋ค์ด ์์๋ค.
ํด๋น ๋ก์ง์ผ๋ก ํ์ํ ํธ๋์ญ์ ์ ๋ง๋ค์์ ๋,
์ด๊ฑธ๋ก ๊ฑฐ์ ๋ชจ๋ ํ ์คํธ๊ฐ ํฐ์ ธ๋ฒ๋ฆฌ๋ ๋ฌธ์ ๊ฐ ์์๋ค.
์ด์ ๊ฐ ๋ญ๊น?
jest์์ ํธ๋์ญ์ ์ด ๋ช ์๋์ด์๋ ๊ฒฝ์ฐ์, ์ด๋ฅผ ๋จ์ํ ์คํธ์์ ๋ชจํนํด์ฃผ์ด์ผ ํ๋ค.
jest.mock('typeorm-transactional', () => ({
Transactional: () => () => ({}),
}));
๋ผ๋ ๋ก์ง์ผ๋ก ๋ชจํน์ ๋ชจ๋ ํด๊ฒฐํ๋ค.
์ฐ๋ฆฌ๊ฐ ์ถ๊ฐ์ ์ธ ํธ๋์ญ์ ์ ๋ง๋ ์ ์ด ์๋๋ฐ, ์ ์ด๋ฐ ์๋ฌ๊ฐ ๋์ฌ๊น ๋๋ฌด ๊ณ ๋ฏผํ๋ค.
๊ทผ๋ฐ, ์์ธ์ ๋จ์ํ๋ค.
async save(workbook: Workbook) {
return await this.repository.save(workbook);
}
save๋ผ๋ Type ORM ๊ธฐ๋ณธ ๋ก์ง์, ์ฌ์ค select & update/insert๋ฅผ ํ ํธ๋์ญ์ ์์์ ์ฒ๋ฆฌํด์ค๋ค. (์ฌ์ค ์ด๊ฑด ๋ค๋ค ์๊ฑฐ๋ค)
spring์์ @Transactional์ ์ฌ์ฉํ๊ณ , ์์์ save๋ฅผ ํด๋ ์๋ฌด ๋ฌธ์ ๊ฐ ์์์ง๋ง,
nestjs์์@Transactional์ ์ฌ์ฉํ๊ณ , ์์์ save๋ฅผ ํ๋ฉด ํธ๋์ญ์ ์์ ํธ๋์ญ์ ์ด ์์ ์ ์๋ค๋ ์๋ฌ๋ฅผ ํฐ๋จ๋ ธ๋ค.
๊ทธ๋์, ๋๋ save๋ก์ง์ ์๋์ ๋ฐฉ์์ผ๋ก ๋ฐ๊ฟ์ฃผ๋ ๊ณผ์ ์ ๊ฑฐ์ณค๋ค.
async save(workbook: Workbook) {
await this.repository.insert(workbook);
return this.repository.findOneBy({
title: workbook.title,
member: { id: workbook.member.id },
});
}
์ฝ๋๋ฅผ ๋ฐ๊พธ๊ณ ๋์, save๊ฐ ํธ๋์ญ์ ์์ ์๋ ๊ฒฝ์ฐ์ ๋ํ ๋ฌธ์ ๋ฅผ ์์ ํ๋ค.
์ฌ์ค ์ด๊ฑธ ์ดํดํ๋ ๊ณผ์ ์ ์ ์ผ ์ด๋ ค์ ๋ค.
๋๋์ฒด ์ ์ด๋ฐ ์๋ฌ๊ฐ ๋์์๊น?
async dataSourceFactory(options) {
if (!options) {
throw new Error('Invalid options passed');
}
return addTransactionalDataSource(new DataSource(options));
}
์ด ๋ก์ง๊ณผ jest๊ฐ ๊ฐ์ด ์๋๊ฒ ๋ฌธ์ ์๋ค.
์๊น ํ๋ ๋ฌธ์ฅ์์, ํ๋๋ฅผ ๊ธฐ์ตํด์ผ ํ๋ค.
์ฆ, jest๋ฅผ ์คํํ๋ ํ ์คํธํ๊ฒฝ์์ ์ฐ๋ฆฌ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ํ ์ฐ๊ฒฐ๋ค๋ฆฌ ํ๋๋ง์ ๋ง๋ ๋ค (์ด ๋ ์ด๋ฆ์ ๋ฑ๋กํด์ฃผ์ง ์์ default๋ก ๋ฑ๋ก๋๋ค)
๊ทผ๋ฐ, jest๋ ๋ณ๋ ฌ์ ์ผ๋ก ํ ์คํธ๋ฅผ ํ๋ฉฐ, ๋๋ ํ ์คํธ๋ง๋ค ๋ค๋ฅธ Test Module์ ๋ง๋ ๋ค.
์ฌ๊ธฐ์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ๋ค๋ฆฌ๋ฅผ ๋ค ๋ง๋ค๊ฒ๋๋๋ฐ?
๊ทธ๋์ ํฐ์ก๋ค
๋๋ถ์ jest๊ฐ ๋ชจ๋ ํ ์คํธ๋ฅผ ๋ณ๋ ฌ์ ์ผ๋ก ์งํํ๋ค๋ ๊ฒ,
dataSourceFactory์ ์ง๊ธ ๋ด ๋ก์ง์์๋ ํ๋์ ์ฐ๊ฒฐ๋ค๋ฆฌ๋ก๋ง ํต์ ์ ํ๋ค๋ ์ ์ ์๊ฒ ๋์๋ค
ํ ์คํธ์์ ์ด๊ฑธ ํด๊ฒฐํ๊ธฐ ์ํด ์๋ก์ด ์ฐ๊ฒฐ๋ค๋ฆฌ๋ฅผ ๋ง๋ค ๋, ์ด๋ฏธ ์กด์ฌํ๋ค๋ฉด ๊ทธ๊ฑธ ๊ฐ์ ธ์์ ์ฌ์ฉํ๋ ๊ฒ์ผ๋ก ๋๋ด๊ฒ ์์ ํ๋ค.
async dataSourceFactory(options) {
return (
getDataSourceByName('default') ||
addTransactionalDataSource(new DataSource(options))
);
}
์ด๋ฐ ๋ก์ง์ ํตํด default๋ผ๋ ์ด๋ฆ์ด ์์ผ๋ฉด ํด๋น ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๊ฐ์ ธ์ ์ฌ์ฉํ๊ณ ๋๋ด๊ณ , ์๋ค๋ฉด ์๋กญ๊ฒ default๋ก ๋ง๋ค์ด์ฃผ๋ ๋ก์ง์ผ๋ก ๋๋๋ค. (๋ ํจ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๊ณต ํจ์๋ค)
์ด๋ ๊ฒ ํ๋ก๋์ , ํ ์คํธ ํ๊ฒฝ์์ ๋ชจ๋ ํธ๋์ญ์ ์ฒ๋ฆฌ๋ฅผ ์ฑ๊ณต์์ผฐ๋ค.
ํ ์คํธ๊ฐ 1ํธ๋ณด๋ค 12๊ฐ ์ฆ๊ฐํ๊ณ , ์ ์ฒด ๋ก์ง ์ํ์๊ฐ์ด ์คํ๋ ค 0.3์ด ์ค์ด๋๋ ํจ๊ณผ๋ฅผ ๋ณด์๋ค.
๊ฐ๊ฟ์ด๊ตฌ๋ง?
ํ์ง๋ง, ๋ง๋ฅ ๊ฐ๊ฟ์ ์๋์๋ค. ํธ๋์ญ์ ์ ๋ด ์๊ฐ๋ณด๋ค ๋ ์กฐ์ฌํด์ผํ๋ค.
๋ชจ๋ ๋ก์ง์ @Transactional()์ ๋ฌ๊ณ ์ฒ๋ฆฌํ๋ฉด ์ข์๊น๋ฅผ ๋ฉํ ๋๊ป ์ฌ์ญ์ด๋ณด์์ ๋,
๊ทธ๊ฑด ์๋๋ผ๋ ์ด์ผ๊ธฐ๊ฐ ์์๋ค.
๋๋ Spring์์ ๋ชจ๋ ๋ก์ง์ @Transactional๋ฅผ ๋ฌ์์๋๋ฐ,
์ด๋ก์ธํด ํธ๋์ญ์ ์ด ๊ผฌ์ฌ ๋ฌธ์ ๊ฐ ์์ ์ ์๋ค๊ณ ํ๋ค.
์ด์ ๋ํด ์ฐพ์๋ณด์์ ๋, ๋ชจ๋ ๋ก์ง์ ํธ๋์ญ์ ํ๋ ํฌ๊ฒ ๋ ๊ฐ์ง์ ๋ฌธ์ ๋ฅผ ๊ฐ์ง๊ฒ ํ๋ค๋ ์ ์ ์๊ฒ ๋์๋ค.
1๋ฒ์ ๊ฒฉ๋ฆฌ์์ค ์ค์ , ํธ๋์ญ์ ์ค์ ์ ํ์ธํ๋ฉด์ ์ง๋๊ฐ๋ค๋ฉด ๊ด์ฐฎ๊ฒ ์ง๋ง,
2๋ฒ์ ์ด์ ์์ ํ์คํ ๋ชจ๋ ๋ก์ง์ ํธ๋์ญ์ ํ๊ฐ ๋ง๋ฅ ์ข์ง ์๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค๊ธฐ๋ ํ๋ค.
๋ชจ๋ ์ฟผ๋ฆฌ์ ๋ํ ๋กค๋ฐฑ์ด ํ์คํ ํ์ํ ๋,
๊ทธ๋ฆฌ๊ณ ๊ฒฉ๋ฆฌ์์ค์ด ์ค์ ๋์ด์ผ ํ ๊ฒฝ์ฐ์๋ง ํด๋น ํธ๋์ญ์ ์ด ๋์๋๊ฒ ํ๋ ๊ฒ์ด ์ข๋ค๋ ์๊ฐ์ด ๋ ๋ค.
์์ผ๋ก ํธ๋์ญ์ ์ ๊ฒฉ๋ฆฌ ์์ค์ ํ์์ฑ์ด ์์ ๋, ํ์คํ๊ฒ ์ฑ๋ฅ์ ํฅ์์ด ์๋์ง ๊ฒ์ฆํ๊ณ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข๊ฒ ๋ค๋ ์๊ฐ์ด ๋ ๋ค.
๊ทธ๋ผ...twenty thousand...๐ฅ