๐ก 05. Drizzle ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋ โ ์์ ์ ์ธ ํ์ ์์ ์ฑ๊ณผ DI์ ๋ง๋จ
๐ ๊ฐ์
NestJS์ ์์กด์ฑ ์ฃผ์ ์์คํ ์ ํ์ฉํ์ฌ Drizzle ORM์ ๊ฐ์ฅ ์ฐ์ํ๊ณ ์ฒด๊ณ์ ์ผ๋ก ํ๋ก์ ํธ์ ๋ น์ฌ๋ด๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ ์ ์ญ Database ๋ชจ๋ ์ฃผ์กฐํ๊ธฐ ๐ก
- ๐งฉ ํธ๋์ญ์ ๊ณผ Repository ํจํด ๐ข
- ๐งช ๋ฐ๋ผํด๋ณด๊ธฐ: ์ค์ต ์ฝ๋ ์กฐ๊ฐ ๋ชจ์
- ๐ผ ๋ฒ ์คํธ ํ๋ํฐ์ค์ ์ค๋ฌด ํ
- โ ๏ธ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐๏ธ ์นํธ์ํธ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 20๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 10๋ถ
๐งณ ์ ์ ์ง์ ์ฒดํฌ๋ฆฌ์คํธ
- 03์ฅ์ ์์กด์ฑ ์ฃผ์
(DI) ๊ณผ
@Injectable()์ ๊ฐ๋ ์ ๋ช ํํ ์ดํดํ๊ณ ์๋ค. - Drizzle ORM์ ๊ธฐ๋ณธ ๋ฌธ๋ฒ(
select,insert,eq)์ ์ด๋ ์ ๋ ์ฝ์ ์ค ์๋ค. - PostgreSQL๊ณผ ๊ฐ์ ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ 'ํธ๋์ญ์ (Transaction)'์ด ๋ฌด์์ธ์ง ์๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
๋
๋ฆฝํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Drizzle์ NestJS ์ํ๊ณ์ ํธ์
์ํค๊ธฐ โ ์๋น์ค์์ DB ์ฃผ์
๋ฐ์ ์ฐ๊ธฐ โ ํธ๋์ญ์
๋ค๋ฃจ๊ธฐ โ ๋๊ท๋ชจ ์ฑ์ ์ํ Repository ๋ถ๋ฆฌ โ ํ
์คํธ Mocking ๋ฐฉ์
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
- Drizzle ์ฐ๋์ ์ํ ์ ์ญ ๋ชจ๋(Global Module) ํจํด์ ์๋ฒฝํ ๊ตฌ์ถํ ์ ์๋ค.
- ์ฌ๋ฌ ์ฟผ๋ฆฌ๊ฐ ๋ชจ๋ ์ฑ๊ณตํ๊ฑฐ๋ ๋ชจ๋ ์คํจํด์ผ๋ง ํ๋ 'ํธ๋์ญ์ ' ์ฝ๋๋ฅผ ์งค ์ ์๋ค.
- ํ ์คํธ ์ฝ๋ ์์ฑ ์ ์ค์ DB๋ฅผ ๊ฑด๋๋ฆฌ์ง ์๋๋ก Mock ๊ฐ์ฒด๋ฅผ ์ฃผ์ ํ ์ ์๋ค.
๐ค ์ ์์์ผ ํ๋๊ฐ
Drizzle์ TypeORM์ด๋ Prisma์ฒ๋ผ ๋ฉ์น๊ฐ ํฐ 'ํ๋ ์์ํฌ'๊ฐ ์๋๋ผ, ๊ฐ๋ณ๊ฒ SQL์ ๋์์ฃผ๋ '๋ผ์ด๋ธ๋ฌ๋ฆฌ(Query Builder)'์ ๊ฐ๊น์.
๊ทธ๋์ "์ค์ ํ์ผ ํ๋ ๋ฑ!" ํ๊ณ ๋์ ธ์ฃผ๋ ํธ์ํ ๋ด์ฅ ๋ชจ๋์ด ์์ง ๋น์ฝํ ํธ์ด์ง. ๊ทธ๋ ๋ค๊ณ const db = drizzle(...) ํด๋๊ณ ํ์ผ๋ง๋ค ๋ฌด์์ import ํด์ ์ฐ๋ค๊ฐ๋, ํ
์คํธ(Mocking)๊ฐ ๋ถ๊ฐ๋ฅํด์ง๋ ๋ฌธ์ ์ํฉ ๋ฅผ ๊ฒช๊ฒ ๋ผ.
๊ทธ๋์ Drizzle์ NestJS์ DI(์์กด์ฑ ์ฃผ์
) ์์คํ
์์ ์์๊ฒ ํฌ์ฅํ๋ ๊ธฐ์ ์ ๋ฐ๋์ ์์์ผ ํด.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
๋ฐ์ดํฐ๋ฒ ์ด์ค(DB)๋ ์ฐ๋ฆฌ ํ์ฌ ๋ฐ ์ ๋ฉ๋ฆฌ ์๋ '๊ฑฐ๋ํ ์ฐฝ๊ณ '์ผ. ์ฐฝ๊ณ ์ ๋ฌผ๊ฑด์ ๋ฃ๊ฑฐ๋ ๋นผ๋ ค๋ฉด ํน๋ณํ ์ ์ฉ ํซ๋ผ์ธ ์ ํ๊ธฐ ๊ฐ ํ์ํด.
๋์ ๋ฐฉ๋ฒ (๊ธ๋ก๋ฒ ์ง์ import):
100๋ช
์ ์ง์์ด ๊ฐ์ ์๊ธฐ ์๋ฆฌ์ ์ ํ๊ธฐ๋ฅผ ๋ชฐ๋ ์ค์นํด์ ์ฐฝ๊ณ ์ ์ ํ๋ฅผ ๊ฑธ์ด. ์ ํ์ ์ด ์์๋ก ์ํค๊ณ ์ฐฝ๊ณ ์์ ์จ(Max Connections)๋ ํ๋ด๋ฉด์ ์ ํ๋ฅผ ๋ค ๋์ด๋ฒ๋ ค. ๊ฒ๋ค๊ฐ '์ฐ์ต์ฉ ์ฐฝ๊ณ (ํ
์คํธ ๋ชจ๋)'๋ก ์ ํ๋ฅผ ๋๋ฆด ๋ฐฉ๋ฒ๋ ์์ด.
์ข์ ๋ฐฉ๋ฒ (NestJS ํ์ดํ๋ผ์ธ + DI):
ํ์ฌ 'ํต์ ๋ถ์(DatabaseModule)'์์ ๋ฑ ํ๋์ ์ต๊ณ ๊ธ ์ ํ๊ธฐ(drizzle ์ธ์คํด์ค)๋ฅผ ๊ฐํตํด. ๊ทธ๋ฆฌ๊ณ ์ ํ๋ฅผ ์จ์ผ ํ๋ ์ง์(Service)์ด ์ถ๊ทผํ๋ฉด "์ด๋ฐ ์ฐฝ๊ณ ์ ํ๊ธฐ ์ข ์ธ๊ฒ์!" ํ๊ณ ๋ณธ๋ถ์ ์์ฒญํด. ๊ทธ๋ฌ๋ฉด ์ง๋ฐฐ์ธ์ด ๊น๋ํ๊ฒ ์ฐ๊ฒฐํด์ฃผ๋ ๊ฑฐ์ง.
๐งฉ ์ ์ญ Database ๋ชจ๋ ์ฃผ์กฐํ๊ธฐ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
useFactory๋ผ๋ ์ปค์คํ ํ๋ก๋ฐ์ด๋์ ๊ฐ๋ ์ ์ดํดํ๊ณ , ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ NestJS์ฉ์ผ๋ก ๊ฐ์ ์ ์๋ค.
๊ฐ์ฅ ๋จผ์ ์ด "์ต๊ณ ๊ธ ์ ํ๊ธฐ"๋ฅผ ์ธํ ํ๋ ํต์ ๋ถ์๋ฅผ ๋ง๋ค์. ์๋ ์ฝ๋๋ NestJS + Drizzle ์ฐ๋์ ๊ต๊ณผ์ ์ผ.
// database/database.module.ts
import { Global, Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from '@repo/schema';
// ์ด ๋ฌธ์์ด์ด ์๋น์์ ์
ฐํ(DB)๋ฅผ ๋ถ๋ฅผ ๋ ์ฐ๋ ๋ช
์ฐฐ(Token)์ด์ผ!
export const DATABASE_TOKEN = 'DATABASE';
@Global() // ๐ ์ ์ญ ๋ชจ๋ ์ ์ธ! ๋ค๋ฅธ ๋ถ์์์ Imports ์ ํ๊ณ ๊ฐ์ ธ๋ค ์ธ ์ ์์
@Module({
providers: [
{
provide: DATABASE_TOKEN, // "๋๊ฐ ์ด ๋ช
์ฐฐ์ ๋ถ๋ฅด๋ฉด..."
// "...์ด ํฉํ ๋ฆฌ ํจ์๋ฅผ ํ ๋ฒ๋ง ์คํํด์ ์ง์ง ์ ํ๊ธฐ๋ฅผ ์ฅ์ด์ค๋ผ!"
useFactory: (configService: ConfigService) => {
const url = configService.getOrThrow<string>('DATABASE_URL');
// 1. ์ค์ Postgres ์ฐ๊ฒฐ ํด๋ผ์ด์ธํธ ์์ฑ
const client = postgres(url, { max: 10 });
// 2. Drizzle๋ก ๋ํํ๊ณ , ์ฐ๋ฆฌ๊ฐ ๋ง๋ ์คํค๋ง(ํ
์ด๋ธ ๊ตฌ์กฐ) ์ฃผ์
return drizzle(client, { schema });
},
// ํฉํ ๋ฆฌ ํจ์๊ฐ ์คํ๋๊ธฐ ์ ์ ConfigService๊ฐ ๋จผ์ ์กฐ๋ฆฝ๋์ด์ผ ํจ์ ๋ปํด
inject: [ConfigService],
},
],
exports: [DATABASE_TOKEN], // ์ฐ๋ฆฌ ๋ถ์ ๋ฐ์ผ๋ก ๊ฐ๋ฐฉ!
})
export class DatabaseModule {}๐ ํต์ฌ ๊ฐ๋ :
useFactory
๋ด๊ฐ ์ง์ ๋ง๋ ํด๋์ค๊ฐ ์๋, ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํจํค์ง(drizzle)๋ ๋ด ๋ง๋๋ก ์ฝ๋์@Injectable()๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋ฌ์์ค ์๊ฐ ์์์? ๊ทธ๋ด ๋ ์ ๋ ๊ฒ ๊ณต์ฅ(Factory) ํจ์๋ฅผ ๋ง๋ค์ด์ "์ด ํจ์๊ฐ ๋ฑ์ด๋ด๋ ๊ฒฐ๊ณผ๋ฌผ์ ๋์ ์ฃผ์ ํด ์ค!" ๋ผ๊ณ ์ง์ ํ ์ ์์ด. ์ด๊ฒ ๋ฐ๋ก ์ปค์คํ ํ๋ก๋ฐ์ด๋์ผ.
๐งฉ ํธ๋์ญ์ ๊ณผ Repository ํจํด ๐ข
DB ์ฐ๊ฒฐ์ ๋ง์ณค์ผ๋, ์๋น์ค์์ ์ค์ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ ค๋ณผ ์๊ฐ์ด์ผ!
1. ๊ธฐ๋ณธ ์ฃผ์ ๊ณผ ํธ๋์ญ์ (Transaction)
์ ์ ๊ฐ ๊ฒฐ์ ๋ฅผ ํ์ด. 1๋ฒ: ์ ์ ์ง๊ฐ์์ ๋์ด ๊น์. 2๋ฒ: ์ฃผ๋ฌธ์ด ์์ฑ๋จ.
๊ทธ๋ฐ๋ฐ 1๋ฒ์ ์ฑ๊ณตํ๊ณ 2๋ฒ์ ์๋ํ๋ค ์๋ฒ๊ฐ ๊บผ์ก์ด. ์ ์ ๋๋ง ๋ ์๊ฐ์ง? ์ด๊ฑธ ๋ง๊ธฐ ์ํด ๋ ์์
์ "๋์์ ์ฑ๊ณตํ๊ฑฐ๋ ๋์์ ์คํจ(Rollback)"ํ๊ฒ ๋ฌถ๋ ๊ฒ์ด ํธ๋์ญ์
์ด์ผ.
import { Injectable, Inject } from '@nestjs/common';
import { DATABASE_TOKEN, Database } from '../database';
// ...๊ธฐํ import
@Injectable()
export class OrderService {
constructor(
@Inject(DATABASE_TOKEN) // ์๊น ๋ง๋ ๋ช
์ฐฐ๋ก DB ์ ํ๊ธฐ๋ฅผ ์ฃผ์
๋ฐ์!
private readonly db: Database,
) {}
async createOrder(userId: string, totalAmount: number): Promise<void> {
// ๐ฅ ์ด ๋ธ๋ก ์์์ ์ผ์ด๋๋ ์ผ์ ์ ๋ถ ์คํจํ๊ฑฐ๋ ์ ๋ถ ์ฑ๊ณตํจ
await this.db.transaction(async (tx) => {
// 1. ์ ์ ์๊ณ ๊ฐ์ (์ด๋ this.db๊ฐ ์๋๋ผ tx๋ฅผ ์จ์ผ ํจ ์ฃผ์!)
await tx.update(users)
.set({ balance: sql`${users.balance} - ${totalAmount}` })
.where(eq(users.id, userId));
// 2. ์ฃผ๋ฌธ ์ ๋ณด ์์ฑ
await tx.insert(orders).values({ userId, totalAmount });
// ๋ง์ฝ ์ฌ๊ธฐ์ ์๋ฌ๊ฐ ๋๋ฉด(throw new Error) 1๋ฒ์ ์ง๊ฐ ์ฐจ๊ฐ๋ ์๋ ์์๋ณต๊ตฌ!
});
}
}2. Repository ํจํด์ผ๋ก์ ๋ถ๋ฆฌ (๋๊ท๋ชจ ์ฑ)
OrderService ํ์ผ ํ๋์ ์ฃผ๋ฌธ ์๋ฆผ ๋ณด๋ด๊ธฐ ๋ก์ง, ํฌ์ธํธ ์ ๋ฆฝ ๋ก์ง ๋ฑ ๋น์ฆ๋์ค ์ฝ๋๊ฐ ๋๋ฌด ๊ธธ์ด์ง๋ฉด, ์์ ๊ฐ์ SQL ์ฟผ๋ฆฌ ์ฝ๋๋ ๋์ ๊ฑฐ์ฌ๋ฆฌ๊ธฐ ์์ํด. ๊ทธ๋๋ ๋ฐ์ดํฐ ์ ๊ทผ๋ง ์ ๋ดํ๋ Repository(์ ์ฅ์ ๋ด๋น์) ๋ฅผ ๋ฐ๋ก ๋ง๋ค์ด.
// ๐ stocks.repository.ts
@Injectable()
export class StocksRepository {
constructor(@Inject(DATABASE_TOKEN) private db: Database) {}
findBySymbol(symbol: string) {
return this.db.query.stocks.findFirst({ where: eq(stocks.symbol, symbol) });
}
}
// ๐ stocks.service.ts
@Injectable()
export class StocksService {
// DB ์ ํ๊ธฐ ๋์ Repository ๋ด๋น์๋ฅผ ์ฃผ์
๋ฐ์!
constructor(private readonly stocksRepo: StocksRepository) {}
async getPrice(symbol: string) {
const stock = await this.stocksRepo.findBySymbol(symbol);
return stock.currentPrice;
}
}์ด๋ ๊ฒ ํ๋ฉด ์๋น์ค๋ "** ๋น์ฆ๋์ค ๊ท์น**"๋ง ๊ณ ๋ฏผํ๊ณ , ๋ ํฌ์งํ ๋ฆฌ๋ "** ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ๋นจ๋ฆฌ ๋ฝ์์ฌ ์ง**"๋ง ๊ณ ๋ฏผํ๋ ์๋ฆ๋ค์ด ๋ถ์ ์ด ์์ฑ๋ผ.
๐งช ๋ฐ๋ผํด๋ณด๊ธฐ: ์ค์ต ์ฝ๋ ์กฐ๊ฐ ๋ชจ์
ํ ์คํ : Mock ๊ฐ์ฒด ๊ฝ์๋ฃ๊ธฐ
DB ์ฝ๋๋ฅผ ์๋ฒฝํ๊ฒ ๋ถ๋ฆฌํ์ผ๋ ๋ณด์์ด ์์ด์ผ๊ฒ ์ง? ์ด ์ปจํธ๋กค๋ฌ/์๋น์ค ์ ๋(๋จ์) ํ ์คํธ๋ฅผ ์งค ๋, ์ค์ DB๋ฅผ ๋๊ณ ๋ ํ ์คํธ๊ฐ ๊ฐ๋ฅํด์ ธ.
// stocks.service.spec.ts
import { Test } from '@nestjs/testing';
// 1. ๊ฐ์ง DB ์ ํ๊ธฐ ๋ง๋ค๊ธฐ (Mock)
const mockDb = {
query: {
stocks: { findMany: jest.fn().mockResolvedValue([ { symbol: 'AAPL' } ]) },
},
};
describe('StocksService', () => {
let service: StocksService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
StocksService,
// 2. ๊ฐ์ง ์ ํ๊ธฐ๋ฅผ ์ง์ง ๋ช
์ฐฐ(DATABASE_TOKEN)์ ๋ถ์ฌ์ ์ฃผ์
!
{ provide: DATABASE_TOKEN, useValue: mockDb },
],
}).compile();
service = module.get(StocksService);
});
it('์ฃผ์ ๋ชฉ๋ก์ ์ ๊ฐ์ ธ์์ผ ํจ', async () => {
const res = await service.findAll();
expect(res).toEqual([ { symbol: 'AAPL' } ]); // ์ค์ DB ์ ํ๊ณ 0.01์ด ๋ง์ ํ
์คํธ ํต๊ณผ!
});
});๐ผ ๋ฒ ์คํธ ํ๋ํฐ์ค์ ์ค๋ฌด ํ ๐ก
1. Supabase ์ฐ๋ ์ prepare: false๋ ์๋ช
์ค์ด๋ค.
์๋ฒ๋ฆฌ์ค ํ๊ฒฝ์ด๋ Supabase ๊ฐ์ ๊ณณ์ Connection Pooler(6543 ํฌํธ)๋ฅผ ์ฐ๊ฒ ๋๋๋ฐ, ์ด๋ prepare: false ์ต์
์ ๊บผ์ฃผ์ง ์์ผ๋ฉด "Prepared statement ... already exists" ์๋ฌ๊ฐ ๋ฟ์ด์ง๋ฉฐ ์๋ฒ๊ฐ ๋ป์ด๋ฒ๋ ค.
const client = postgres(url, {
max: 10,
prepare: false // ๐ Supabase Transaction Mode์ ์์ ์ ์ธ ๋จ์ง
});2. NestJS Health Check ์ถ๊ฐํ๊ธฐ
๋ด ์๋ฒ๋ฅผ ๋ฐฐํฌํ์ ๋ DB๊ฐ ๋ป์ผ๋ฉด ์๋ฒ ์ฑ๋ "๋ ์ง๊ธ ๋น์ ์์ด์ผ" ๋ผ๊ณ ์ธ์ณ์ผ ์คํ ์ค์ผ์ผ๋ง์ด ๋์ฒํ ์ ์์ด.
// health.controller.ts
@Get('db')
async checkDatabase() {
try {
// ๊ฐ์ฅ ๊ฐ๋ฒผ์ด ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ ค๋ด
await this.db.execute(sql`SELECT 1`);
return { status: 'Database is healthy!' };
} catch (error) {
throw new ServiceUnavailableException('Database unavailable'); // 503 ์๋ฌ
}
}โ ๏ธ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
์๋ฌ ๋ฉ์์ง๊ฐ ๋จ๋ฉด Ctrl+F๋ก ๊ฒ์ํด๋ด.
โ Nest can't resolve dependencies of the StocksService (?).
์์ธ: StocksService์์ @Inject(DATABASE_TOKEN) private db ๋ก ๋ช
์ฐฐ(ํ ํฐ)์ ๋ถ๋ฅด๋ฉฐ ์ฐพ๊ณ ์๋๋ฐ, app.module.ts์ imports ์ชฝ์ DatabaseModule์ ๋ฃ์ง ์์์ NestJS๊ฐ ๊ทธ ๋ช
์ฐฐ์ด ๋ญ์ง ๋ชจ๋ฆ!
(์ฐธ๊ณ : DatabaseModule์ @Global()์ ๋ฌ์๋ค๋ฉด app.module.ts ํ ๊ณณ์๋ง ๋ฑ ํ ๋ฒ import ํด๋๋ฉด ๋ผ!)
โ Remaining connection slots are reserved for non-replication superuser connections
ํ์: ๊ฐ์๊ธฐ DB ์กฐํ ์๋ต์ด ์์ฒญ ๋๋ ค์ง๋๋ ์๋ฌ๊ฐ ๋จ.
์์ธ: postgres ๋๋ผ์ด๋ฒ๋ฅผ AppModule ์ธ๋ถ์์ ๊ธ๋ก๋ฒ ๋ณ์์ฒ๋ผ ๋งค๋ฒ ํจ์ ํธ์ถ๋๋ง๋ค ์ฐ์ด๋ด์ Connection ๊ฐ์ฒด๊ฐ ์๋ฐฑ ๊ฐ ๋๊ฒ ํญ์ฆํ๊ธฐ ๋๋ฌธ.
ํด๊ฒฐ์ฑ
: ๋ฐ๋์ NestJS์ ์ฑ๊ธํค ๋ชจ๋(useFactory) ์์์ ๋จ ํ ๋ฒ๋ง ์์ฑ๋๋๋ก ์ฝ๋๋ฅผ ๊ณ ์ณ์ผ ํด!
๐๏ธ ์นํธ์ํธ โ ์ค๋ฌด ์์ฝ ์นด๋
| ์ํฉ | ์ฝ๋ ์ค๋ํซ | ๋น๊ณ |
|---|---|---|
| ์ปค์คํ ํ ํฐ ์ฃผ์ ๋ฐ๊ธฐ | constructor(@Inject(TOKEN) db) {} | Drizzle ์ฌ์ฉ์ ์์ |
| ํธ๋์ญ์ ์ด๊ธฐ | await db.transaction(async (tx) => { ... }) | ๋ด๋ถ์ tx๋ง ์จ์ผ ํจ |
| ์์ SQL ์คํ | db.execute(sql\SELECT * FROM users`)` | ORM์ผ๋ก ๋ชป ์ง๋ ๋ณต์กํ ์ฟผ๋ฆฌ |
| Mocking ์ฃผ์ (ํ ์คํธ) | { provide: TOKEN, useValue: mockDb } | Test ๋ชจ๋ ๊ตฌ์ฑ ์ |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Drizzle์ ๋ด๊ฐ ์ง์ @Injectable()์ ๋ถ์ผ ์ ์๊ธฐ ๋๋ฌธ์ ์ด๋ค ๋ฐฉ์์ผ๋ก ํ๋ก๋ฐ์ด๋๋ฅผ ์์ฑํด์ผ ํ๋๊ฐ?
- A) ์ปจํธ๋กค๋ฌ ์์์ ๋งค์ผ
require('drizzle')์ ํธ์ถํ๋ค. - B)
@Global()๋ฐ์ฝ๋ ์ดํฐ๋ง ๋ฌ๋ฉด ์์์ ์ฃผ์ ๋๋ค. - C)
useFactoryํจ์๋ฅผ ์จ์ ์ฝ๋๋ก ์ธ์คํด์ค๋ฅผ ์ฐ์ด๋ด ๋ฐํํ๋ค. - D)
useClass์์ฑ์ ์ด๋ค.
โ ์ ๋ต: C
๐ก ์ค๋ช : ์ปค์คํ ํ๋ก๋ฐ์ด๋์ ์ ์! ํฉํ ๋ฆฌ๋ฅผ ํตํด ๊ณต์ฅ์ ๋๋ ค์ ์ฐ์ด๋ธ ๊ฒฐ๊ณผ๋ฌผ์ ๋์ ธ์ฃผ๋ ๋ฐฉ์์ด์ผ.
Q2. ๊ฒฐ์ ๋น์ฆ๋์ค ๋ก์ง ๋์ค, ๋ค์ ๋ ์ฟผ๋ฆฌ๊ฐ ํ๋์ ํธ๋์ญ์ ์ผ๋ก ๋ฌถ์ฌ์์ง "์์" ๋ ๋ํ๋๋ ๋ฌธ์ ์ ์ผ๋ก ๊ฐ์ฅ ์ ํํ ๊ฒ์?
await this.db.update(users).set({ point: point - 10 }); // ์ ์ ํฌ์ธํธ ์ฐจ๊ฐ
// <--------- (๋ง์ฝ ์ด ํ์ ์๋ฒ๊ฐ ์
ง๋ค์ด๋๋ค๋ฉด?)
await this.db.insert(orders).values({ item: 'Apple' }); // ์ฃผ๋ฌธ 1๊ฑด ์์ฑ- A) ์ปดํ์ผ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
- B) ์ ์ ์ ํฌ์ธํธ๋ ์ฐจ๊ฐ๋์๋๋ฐ, ๋์ ์ง๋ถํ ์ฃผ๋ฌธ ๋ด์ญ(์ํ)์ ์๋ ์ํ๊ฐ ๋๋ค.
- C) Drizzle ORM์ด ์์์ ๋ค์ ์คํํด์ค๋ค.
โ ์ ๋ต: B
๐ก ์ค๋ช
: ๋๋ง ๋จน๊ณ ์์ดํ
์ ์ ์ฃผ๋ ์ํํ ๋ถ์ผ์น ์ํฉ! ํธ๋์ญ์
(db.transaction(tx => ...))์ผ๋ก ์์ชฝ์ ๊ฐ์ ๊ฒฐํฉ์์ผ ํ์ชฝ์ด ๊นจ์ง๋ฉด ๋ฐ๋์ ์ ๋ถ ์์๋ณต๊ตฌ(Rollback) ์์ผ์ผ ํด.
Q3. ๋๋ฒ๊น ํด์ฆ: ์๋ ์ฝ๋์์ ํธ๋์ญ์ ์ด ํ๋๋ ๋์ํ์ง ์๋ ์ด์ ๋ฅผ ์ฐพ์๋ด.
await this.db.transaction(async (tx) => {
await this.db.insert(users).values({ name: 'Kim' }); // 1๋ฒ
await this.db.insert(logs).values({ action: 'signin' }); // 2๋ฒ
});โ
์ ๋ต: ํธ๋์ญ์
๋ธ๋ก ๋ด๋ถ์์ ๊ณ์ ๊ธฐ์กด this.db๋ฅผ ์ฐ๊ณ ์๊ธฐ ๋๋ฌธ!
์ค๋ช
: ํธ๋์ญ์
ํ๊ฒฝ์ ์ด์ด์คฌ์ผ๋ฉด, ๊ทธ ์์์๋ ํ๋ผ๋ฏธํฐ๋ก ๋์ด์จ tx๋ฅผ ์จ์ await tx.insert(...)๋ก ํธ์ถํด์ผ ํ๋์ ํธ๋์ญ์
ํ๋ฆ์ผ๋ก ์ทจ๊ธํด. this.db๋ฅผ ์ฐ๋ฉด ๋ฐ๊นฅ์ ํธ๋์ญ์
๊ณผ ๋ฌด๊ดํ ๋
๋ฆฝ ์ฟผ๋ฆฌ๋ก ๋ ์๊ฐ ๋ฒ๋ ค!
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ NestJS์ Drizzle์ ์ฐ๊ฒฐํ๋ ์ผ์ด ๋จ์ํ DB ๊ฐ์ฒด๋ฅผ importํ๋ ๋ฌธ์ ๊ฐ ์๋๋ผ๋ ๊ฑธ ๋ฐฐ์ ๋ค. ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ useFactory๋ก ํ๋ก๋ฐ์ด๋๋ฅผ ๋ง๋ค๋ฉด Nest์ DI ํ๋ฆ ์์ ๋ค์ด์จ๋ค.
ํธ๋์ญ์ ์์ ๋ ํนํ ์ธ๊ฒ ๋จ์๋ค. ์์๋ค ํฌ์ธํธ ๊ฒฐ์ ์์ ํฌ์ธํธ๋ง ๋น ์ง๊ณ ์ฃผ๋ฌธ์ด ์์ผ๋ฉด ๊ทธ๊ฑด ์ฝ๋ ๋ฒ๊ทธ๊ฐ ์๋๋ผ ๋ฐ๋ก ์ด์ ์ฌ๊ณ ๋ค.
๐ก "DB ์ฐ๋์ ํต์ฌ์ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฌ๋ ๋ฒ์ด ์๋๋ผ, ์คํจ ๋จ์์ ์ฃผ์ ๊ฒฝ๊ณ๋ฅผ ๋ช ํํ ๋ง๋๋ ๋ฒ์ด๋ค."
๋ด์ผ์ ์๋น์ค ์ฝ๋์์ ์ฌ๋ฌ ์ฟผ๋ฆฌ๊ฐ ์ด์ด์ง๋ฉด ๋จผ์ ํธ๋์ญ์ ๊ฒฝ๊ณ๋ฅผ ํ์ธํด์ผ๊ฒ ๋ค.