CoreDatabaseModule 구현 가이드라인
1. 개요
CoreDatabaseModule은 DTA-WIDE 프로젝트의 데이터베이스 연결 및 Prisma ORM 설정을 담당하는 핵심 라이브러리입니다. Prisma를 사용하여 데이터베이스 액세스를 추상화하고, 모든 도메인 모듈에서 일관된 데이터베이스 접근 방식을 제공합니다.
2. 설계 원칙
- 단일 책임 원칙: 데이터베이스 연결 관리만 담당
- 타입 안전성: Prisma의 타입 시스템을 활용한 강력한 타입 안전성 제공
- 확장성: 다양한 데이터베이스 시스템 지원
- 재사용성: 여러 도메인에서 재사용 가능한 구조
- 트랜잭션 관리: 일관된 트랜잭션 관리 제공
- 마이그레이션 지원: 데이터베이스 스키마 변경 관리
3. 주요 컴포넌트
3.1 PrismaService
Prisma 클라이언트 인스턴스를 제공하는 서비스
3.2 DatabaseService
데이터베이스 연결 및 작업을 관리하는 서비스
3.3 TransactionManager
트랜잭션 관리를 위한 유틸리티
3.4 MigrationService
데이터베이스 스키마 마이그레이션 관리
4. 구현 가이드
4.1 프로젝트 구조
libs/core/database/
├── src/
│ ├── lib/
│ │ ├── database.module.ts
│ │ ├── prisma.service.ts
│ │ ├── database.service.ts
│ │ ├── transaction/
│ │ │ ├── transaction.manager.ts
│ │ │ └── transaction.decorator.ts
│ │ ├── migration/
│ │ │ ├── migration.service.ts
│ │ │ └── migration.runner.ts
│ │ ├── utils/
│ │ │ ├── connection.util.ts
│ │ │ └── query.util.ts
│ │ └── index.ts
│ └── index.ts
├── prisma/
│ ├── schema.prisma
│ ├── migrations/
│ └── seeds/
└── test/
4.2 Prisma 설정
schema.prisma:
// libs/core/database/prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
output = "../node_modules/.prisma/client"
}
// 공통 모델 정의...
model Example {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
4.3 PrismaService 구현
// libs/core/database/src/lib/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { ConfigService } from '@core/config';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
constructor(private readonly configService: ConfigService) {
super({
datasources: {
db: {
url: configService.getRequired<string>('DATABASE_URL'),
},
},
log: configService.isDevelopment()
? ['query', 'info', 'warn', 'error']
: ['error'],
});
}
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
async enableShutdownHooks(app: any) {
// 애플리케이션 종료 시 정상적으로 DB 연결 종료
this.$on('beforeExit', async () => {
await app.close();
});
}
async cleanDatabase() {
// 테스트용 데이터베이스 정리
if (this.configService.isTest()) {
const modelNames = Reflect.ownKeys(this).filter((key) => {
return key[0] !== '_' && key[0] !== '$' && key !== 'configService';
});
return Promise.all(
modelNames.map((modelName) => {
return this[modelName as string].deleteMany();
}),
);
}
}
}
4.4 DatabaseService 구현
// libs/core/database/src/lib/database.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
/**
* DatabaseService는 데이터베이스 연결 상태 확인 등
* Prisma 클라이언트를 직접 사용하는 것 이상의 추가적인 기능을 제공할 수 있습니다.
* 일반적인 리포지토리나 서비스에서는 PrismaService를 직접 주입하는 것을 권장하지만,
* DB 헬스 체크 등이 필요할 때 이 서비스를 사용할 수 있습니다.
*/
@Injectable()
export class DatabaseService {
constructor(private readonly prisma: PrismaService) {}
/**
* Prisma 클라이언트 인스턴스를 반환합니다.
* (참고: 보통 서비스/리포지토리에서는 PrismaService를 직접 주입합니다)
*/
getPrismaClient() {
return this.prisma;
}
/**
* 데이터베이스 헬스 체크를 수행합니다.
*/
async healthCheck(): Promise<boolean> {
try {
await this.prisma.$queryRaw`SELECT 1`;
return true;
} catch (error) {
// 헬스 체크 실패 시 로깅 추가 고려
console.error('Database health check failed:', error);
return false;
}
}
}
4.5 트랜잭션 관리
// libs/core/database/src/lib/transaction/transaction.manager.ts
import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma.service';
@Injectable()
export class TransactionManager {
constructor(private readonly prisma: PrismaService) {}
/**
* 트랜잭션 내에서 콜백 함수를 실행합니다.
*/
async runInTransaction<T>(
callback: (tx: Prisma.TransactionClient) => Promise<T>,
): Promise<T> {
return this.prisma.$transaction(async (tx) => {
return callback(tx);
});
}
/**
* 트랜잭션 내에서 여러 작업을 배치로 처리합니다.
*/
async runInBatchTransaction<T>(
operations: Prisma.PrismaPromise<any>[],
): Promise<any[]> {
return this.prisma.$transaction(operations);
}
}
// libs/core/database/src/lib/transaction/transaction.decorator.ts
import { applyDecorators, SetMetadata } from '@nestjs/common';
export const TRANSACTION_KEY = 'TRANSACTION';
export function Transaction() {
return applyDecorators(SetMetadata(TRANSACTION_KEY, true));
}
4.6 마이그레이션 서비스
// libs/core/database/src/lib/migration/migration.service.ts
import { Injectable } from '@nestjs/common';
import { exec } from 'child_process';
import { promisify } from 'util';
import { ConfigService } from '@core/config';
const execAsync = promisify(exec);
@Injectable()
export class MigrationService {
constructor(private readonly configService: ConfigService) {}
/**
* 마이그레이션을 실행합니다.
*/
async runMigrations(): Promise<string> {
try {
const { stdout } = await execAsync('npx prisma migrate deploy');
return stdout;
} catch (error) {
throw new Error(`마이그레이션 실행 중 오류 발생: ${error.message}`);
}
}
/**
* 새 마이그레이션을 생성합니다.
*/
async createMigration(name: string): Promise<string> {
if (this.configService.isProduction()) {
throw new Error('프로덕션 환경에서는 마이그레이션을 생성할 수 없습니다.');
}
try {
const { stdout } = await execAsync(
`npx prisma migrate dev --name ${name} --create-only`,
);
return stdout;
} catch (error) {
throw new Error(`마이그레이션 생성 중 오류 발생: ${error.message}`);
}
}
/**
* 마이그레이션 이력을 조회합니다.
*/
async getMigrationHistory(): Promise<string> {
try {
const { stdout } = await execAsync('npx prisma migrate status');
return stdout;
} catch (error) {
throw new Error(`마이그레이션 이력 조회 중 오류 발생: ${error.message}`);
}
}
}
4.7 모듈 구현
// libs/core/database/src/lib/database.module.ts
import { DynamicModule, Module, Global } from '@nestjs/common';
import { ConfigModule } from '@core/config';
import { PrismaService } from './prisma.service';
import { DatabaseService } from './database.service';
import { TransactionManager } from './transaction/transaction.manager';
import { MigrationService } from './migration/migration.service';
@Global()
@Module({})
export class CoreDatabaseModule {
static forRoot(): DynamicModule {
return {
module: CoreDatabaseModule,
imports: [ConfigModule],
providers: [
PrismaService,
DatabaseService,
TransactionManager,
MigrationService,
],
exports: [
PrismaService,
DatabaseService,
TransactionManager,
MigrationService,
],
};
}
}
5. 사용 예시
5.1 모듈에서 사용
// apps/dta-wide-api/src/app/app.module.ts
import { Module } from '@nestjs/common';
import { CoreConfigModule } from '@core/config';
import { CoreDatabaseModule } from '@core/database';
@Module({
imports: [
CoreConfigModule.forRoot(),
CoreDatabaseModule.forRoot(),
// 다른 모듈 임포트...
],
})
export class AppModule {}
5.2 서비스에서 사용
서비스나 리포지토리에서는 일반적으로 PrismaService를 직접 주입하여 사용합니다. 트랜잭션 관리가 필요한 경우, TransactionManager를 추가로 주입하여 사용할 수 있습니다.
// libs/feature/user/src/lib/user.service.ts
import { Injectable } from '@nestjs/common';
import { User, Prisma } from '@prisma/client';
import { PrismaService, TransactionManager } from '@core/database';
@Injectable()
export class UserService {
constructor(
private readonly prisma: PrismaService,
private readonly transactionManager: TransactionManager,
) {}
async findById(id: string): Promise<User | null> {
return this.prisma.user.findUnique({
where: { id },
});
}
async createUser(data: Prisma.UserCreateInput): Promise<User> {
return this.transactionManager.runInTransaction(async (tx) => {
const user = await tx.user.create({
data,
});
await tx.profile.create({
data: {
userId: user.id,
},
});
return user;
});
}
async createMultipleUsers(users: Prisma.UserCreateInput[]): Promise<User[]> {
const operations = users.map((userData) =>
this.prisma.user.create({ data: userData })
);
return this.transactionManager.runInBatchTransaction(operations);
}
}
6. 마이그레이션 관리
마이그레이션 관리에 대한 상세 내용은 prisma-migration-guide.md 문서를 참조하세요. 이 문서에는 아래 내용이 포함되어 있습니다:
- 마이그레이션 명령어 사용법
- 개발 및 프로덕션 환경에서의 마이그레이션 전략
- 자동화된 마이그레이션 실행 방법
- 마이그레이션 문제 해결 가이드
7. 테스트 전략
7.1 테스트 설정
// libs/core/database/test/test-database.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@core/config';
import { PrismaService } from '../src/lib/prisma.service';
import { DatabaseService } from '../src/lib/database.service';
import { TransactionManager } from '../src/lib/transaction/transaction.manager';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: '.env.test',
}),
],
providers: [PrismaService, DatabaseService, TransactionManager],
exports: [PrismaService, DatabaseService, TransactionManager],
})
export class TestDatabaseModule {}
7.2 단위 테스트
// libs/core/database/test/database.service.spec.ts
import { Test } from '@nestjs/testing';
import { DatabaseService } from '../src/lib/database.service';
import { PrismaService } from '../src/lib/prisma.service';
import { ConfigService } from '@core/config';
describe('DatabaseService', () => {
let service: DatabaseService;
let prismaService: PrismaService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [
DatabaseService,
{
provide: PrismaService,
useValue: {
$queryRaw: jest.fn().mockResolvedValue([{ result: 1 }]),
},
},
{
provide: ConfigService,
useValue: {
get: jest.fn(),
getRequired: jest.fn(),
isDevelopment: jest.fn().mockReturnValue(false),
isTest: jest.fn().mockReturnValue(true),
},
},
],
}).compile();
service = moduleRef.get<DatabaseService>(DatabaseService);
prismaService = moduleRef.get<PrismaService>(PrismaService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should perform health check', async () => {
const result = await service.healthCheck();
expect(result).toBe(true);
expect(prismaService.$queryRaw).toHaveBeenCalled();
});
});
7.3 통합 테스트
// libs/feature/user/test/user.service.spec.ts
import { Test } from '@nestjs/testing';
import { UserService } from '../src/lib/user.service';
import { TestDatabaseModule } from '@core/database/test';
import { DatabaseService, TransactionManager } from '@core/database';
describe('UserService (Integration)', () => {
let service: UserService;
let databaseService: DatabaseService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [TestDatabaseModule],
providers: [UserService],
}).compile();
service = moduleRef.get<UserService>(UserService);
databaseService = moduleRef.get<DatabaseService>(DatabaseService);
// 테스트 전 데이터베이스 초기화
await databaseService.getPrismaClient().cleanDatabase();
});
afterAll(async () => {
// 모든 테스트 완료 후 연결 종료
await databaseService.getPrismaClient().$disconnect();
});
it('should create a user with profile', async () => {
const user = await service.createUser({
email: 'test@example.com',
name: 'Test User',
});
expect(user).toBeDefined();
expect(user.email).toBe('test@example.com');
// 연관된 프로필 확인
const profile = await databaseService.getPrismaClient().profile.findUnique({
where: { userId: user.id },
});
expect(profile).toBeDefined();
expect(profile.userId).toBe(user.id);
});
});
8. 모범 사례
8.1 Prisma 모델 정의 가이드라인
- 모든 테이블에 id, createdAt, updatedAt 필드를 포함합니다.
- 관계는 명시적으로 정의하고 외래 키 제약 조건을 설정합니다.
- 인덱스를 적절히 활용하여 성능을 최적화합니다.
- 열거형은 가능한 Prisma 스키마 내에서 정의합니다.
// 모범 사례 예시
model User {
id String @id @default(uuid())
email String @unique
name String
role Role @default(USER)
posts Post[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email, name])
}
model Profile {
id String @id @default(uuid())
bio String?
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum Role {
USER
ADMIN
EDITOR
}
8.2 성능 최적화 팁
- 필요한 필드만 선택:
select절을 사용하여 필요한 필드만 가져옵니다. - 페이지네이션 활용: 대량의 데이터는
take와skip을 사용하여 페이지네이션 처리합니다. - 관계 데이터 필요시에만 로드:
include를 필요한 경우에만 사용합니다. - 인덱스 활용: 자주 검색하는 필드에 인덱스를 추가합니다.
- 데이터베이스 연결 풀 관리: 적절한 연결 풀 크기를 설정합니다.
9. 변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|---|---|---|---|
| 0.1.0 | 2025-03-22 | bok@weltcorp.com | 최초 작성 |