๐Ÿ“ก 05. Drizzle ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๋™ โ€” ์™„๋ฒฝํ•œ ํƒ€์ž… ์•ˆ์ „์„ฑ๊ณผ DI์˜ ๋งŒ๋‚จ

2026๋…„ 2์›” 26์ผ ์ˆ˜์ •๋จ

๐Ÿ“‹ ๊ฐœ์š”

NestJS์˜ ์˜์กด์„ฑ ์ฃผ์ž… ์‹œ์Šคํ…œ์„ ํ™œ์šฉํ•˜์—ฌ Drizzle ORM์„ ๊ฐ€์žฅ ์šฐ์•„ํ•˜๊ณ  ์ฒด๊ณ„์ ์œผ๋กœ ํ”„๋กœ์ ํŠธ์— ๋…น์—ฌ๋‚ด๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 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๋ฅผ ์“ฐ๋ฉด ๋ฐ”๊นฅ์˜ ํŠธ๋žœ์žญ์…˜๊ณผ ๋ฌด๊ด€ํ•œ ๋…๋ฆฝ ์ฟผ๋ฆฌ๋กœ ๋‚ ์•„๊ฐ€ ๋ฒ„๋ ค!


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ