멀티 DataSource가 필요한 상황
실무에서 단일 데이터베이스만 사용하는 경우는 드뭅니다. 읽기 전용 레플리카로 조회 트래픽을 분산하거나, 레거시 DB에서 데이터를 마이그레이션하거나, 멀티 테넌트 환경에서 테넌트별 DB를 분리해야 합니다. TypeORM의 DataSource는 이런 멀티 DB 연결을 체계적으로 관리할 수 있습니다.
DataSource 기본 구조
TypeORM 0.3+에서 Connection은 deprecated되고 DataSource가 표준입니다.
import { DataSource } from 'typeorm';
// 메인 DB
export const mainDataSource = new DataSource({
name: 'main',
type: 'postgres',
host: 'primary.db.internal',
port: 5432,
database: 'app_main',
username: 'app',
password: process.env.DB_PASSWORD,
entities: [User, Order, Product],
synchronize: false,
logging: ['error', 'warn'],
poolSize: 20,
});
// 읽기 전용 레플리카
export const readDataSource = new DataSource({
name: 'read',
type: 'postgres',
host: 'replica.db.internal',
port: 5432,
database: 'app_main',
username: 'app_readonly',
password: process.env.DB_READ_PASSWORD,
entities: [User, Order, Product],
synchronize: false,
logging: false,
poolSize: 30,
});
// 초기화
await mainDataSource.initialize();
await readDataSource.initialize();
NestJS 멀티 DataSource 설정
NestJS에서 TypeOrmModule.forRoot()을 여러 번 호출하여 멀티 DataSource를 등록합니다.
// app.module.ts
@Module({
imports: [
// 메인 DB (기본)
TypeOrmModule.forRoot({
name: 'default',
type: 'postgres',
host: 'primary.db.internal',
port: 5432,
database: 'app_main',
entities: [User, Order, Product],
synchronize: false,
}),
// 읽기 전용 레플리카
TypeOrmModule.forRoot({
name: 'read',
type: 'postgres',
host: 'replica.db.internal',
port: 5432,
database: 'app_main',
entities: [User, Order, Product],
synchronize: false,
}),
// 레거시 MySQL DB
TypeOrmModule.forRoot({
name: 'legacy',
type: 'mysql',
host: 'legacy.db.internal',
port: 3306,
database: 'old_system',
entities: [LegacyCustomer, LegacyOrder],
synchronize: false,
}),
UserModule,
OrderModule,
],
})
export class AppModule {}
모듈에서 사용할 DataSource를 forFeature()의 두 번째 인자로 지정합니다.
// user.module.ts
@Module({
imports: [
// default DataSource에서 User 엔티티 등록
TypeOrmModule.forFeature([User, Order]),
// read DataSource에서 User 엔티티 등록
TypeOrmModule.forFeature([User, Order], 'read'),
// legacy DataSource에서 LegacyCustomer 등록
TypeOrmModule.forFeature([LegacyCustomer], 'legacy'),
],
providers: [UserService],
})
export class UserModule {}
서비스에서 멀티 DataSource 사용
@Injectable()
export class UserService {
constructor(
// 기본 DataSource의 Repository
@InjectRepository(User)
private readonly userRepo: Repository<User>,
// read DataSource의 Repository
@InjectRepository(User, 'read')
private readonly userReadRepo: Repository<User>,
// legacy DataSource의 Repository
@InjectRepository(LegacyCustomer, 'legacy')
private readonly legacyRepo: Repository<LegacyCustomer>,
// DataSource 직접 주입
@InjectDataSource('read')
private readonly readDataSource: DataSource,
) {}
// 쓰기: 메인 DB
async createUser(dto: CreateUserDto): Promise<User> {
const user = this.userRepo.create(dto);
return this.userRepo.save(user);
}
// 읽기: 레플리카 DB (트래픽 분산)
async findAll(): Promise<User[]> {
return this.userReadRepo.find({
order: { createdAt: 'DESC' },
take: 100,
});
}
// 레거시 데이터 조회
async findLegacyCustomer(id: number): Promise<LegacyCustomer> {
return this.legacyRepo.findOneBy({ id });
}
// DataSource로 직접 쿼리
async complexReadQuery(): Promise<any> {
return this.readDataSource.query(`
SELECT u.id, u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name
ORDER BY order_count DESC
LIMIT 10
`);
}
}
Read/Write 분리 패턴
TypeORM의 내장 replication 옵션으로 단일 DataSource에서 Read/Write를 자동 분리할 수도 있습니다.
// 단일 DataSource + replication 설정
TypeOrmModule.forRoot({
type: 'postgres',
replication: {
master: {
host: 'primary.db.internal',
port: 5432,
username: 'app',
password: 'master_pass',
database: 'app_main',
},
slaves: [
{
host: 'replica-1.db.internal',
port: 5432,
username: 'app_readonly',
password: 'read_pass',
database: 'app_main',
},
{
host: 'replica-2.db.internal',
port: 5432,
username: 'app_readonly',
password: 'read_pass',
database: 'app_main',
},
],
},
entities: [User, Order, Product],
})
이 설정에서 TypeORM은 자동으로:
- SELECT 쿼리: slaves에 라운드 로빈으로 분배
- INSERT/UPDATE/DELETE: master로 전송
- 트랜잭션 내 모든 쿼리: master로 전송 (일관성 보장)
// 강제로 master에서 읽기 (쓰기 직후 최신 데이터 필요 시)
const user = await userRepo
.createQueryBuilder('user')
.setQueryRunner(
dataSource.createQueryRunner('master')
)
.where('user.id = :id', { id })
.getOne();
트랜잭션과 멀티 DataSource
서로 다른 DataSource 간 트랜잭션은 독립적입니다. 분산 트랜잭션이 필요하면 Saga 패턴을 사용하세요.
@Injectable()
export class MigrationService {
constructor(
@InjectDataSource('default')
private readonly mainDs: DataSource,
@InjectDataSource('legacy')
private readonly legacyDs: DataSource,
) {}
async migrateCustomer(legacyId: number): Promise<void> {
// 레거시 DB에서 읽기
const legacy = await this.legacyDs
.getRepository(LegacyCustomer)
.findOneBy({ id: legacyId });
if (!legacy) throw new NotFoundException();
// 메인 DB에 트랜잭션으로 쓰기
await this.mainDs.transaction(async (manager) => {
const user = manager.create(User, {
name: legacy.customerName,
email: legacy.email,
migratedFrom: 'legacy',
legacyId: legacy.id,
});
await manager.save(user);
// 마이그레이션 로그
const log = manager.create(MigrationLog, {
sourceSystem: 'legacy',
sourceId: legacy.id,
targetId: user.id,
status: 'completed',
});
await manager.save(log);
});
// 레거시 DB에서 마이그레이션 완료 표시
await this.legacyDs
.getRepository(LegacyCustomer)
.update(legacyId, { migrated: true });
}
}
커넥션 풀 튜닝
멀티 DataSource에서는 각 DataSource별 커넥션 풀을 개별 튜닝해야 합니다.
// 메인 DB: 쓰기 + 중요 읽기
{
name: 'default',
type: 'postgres',
extra: {
max: 20, // 최대 커넥션
min: 5, // 최소 유지
idleTimeoutMillis: 30000, // 유휴 커넥션 30초 후 반환
connectionTimeoutMillis: 5000,
},
}
// 읽기 전용: 대량 조회 트래픽
{
name: 'read',
type: 'postgres',
extra: {
max: 50, // 읽기 트래픽이 많으므로 풀 크기 증가
min: 10,
idleTimeoutMillis: 60000,
statement_timeout: '10000', // 10초 쿼리 타임아웃
},
}
// 레거시: 최소한의 연결
{
name: 'legacy',
type: 'mysql',
extra: {
connectionLimit: 5, // MySQL은 connectionLimit 사용
connectTimeout: 10000,
},
}
헬스체크와 모니터링
@Injectable()
export class DbHealthService {
constructor(
@InjectDataSource('default')
private readonly mainDs: DataSource,
@InjectDataSource('read')
private readonly readDs: DataSource,
) {}
async checkHealth(): Promise<Record<string, boolean>> {
const results: Record<string, boolean> = {};
for (const [name, ds] of Object.entries({
main: this.mainDs,
read: this.readDs,
})) {
try {
await ds.query('SELECT 1');
results[name] = true;
} catch {
results[name] = false;
}
}
return results;
// { main: true, read: true }
}
}
관련 글
- NestJS + TypeORM 트랜잭션 — 단일·중첩 트랜잭션 패턴과 격리 수준
- HikariCP 커넥션 풀 심화 튜닝 — JVM 기반 커넥션 풀 최적화 전략 비교