TypeORM을 처음 접할 때 헷갈리기 쉬운 DataSource. 단순한 설정을 넘어서 다양한 DB 환경과 구조를 지원하는 핵심 기능이다. 이 글에서는 DataSource의 개념부터 설정, 사용법, 다중 스키마/복제 환경까지 정리해보았다.
NestJS와 Typeorm을 메인으로 사용하고 있는 저에게 필요한 정보들과 공유가 되었으면 한다.
📌 DataSource란?
• DB와 상호작용하기 위한 설정 객체 • connection pool 또는 초기 connection을 구축 • .initialize()로 연결 시작, .destroy()로 연결 종료 • 일반적으로 애플리케이션 부트 시 initialize()만 호출하고 destroy는 하지 않음
import { DataSource } from "typeorm"
const AppDataSource = new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "test",
password: "test",
database: "test",
})
AppDataSource.initialize()
.then(() => console.log("Data Source initialized"))
.catch((err) => console.error("Initialization error", err))
//전역으로 AppDataSource를 export하면 애플리케이션 어디서든 사용 가능해요.
const MysqlDataSource = new DataSource({ type: "mysql", ... })
const PostgresDataSource = new DataSource({ type: "postgres", ... })
//포트 번호나 엔진에 따라 설정값이 달라지기 때문에 이렇게 분리해서 관리하는 것도 추천됩니다.
import { AppDataSource } from "./app-data-source"
import { User } from "../entity/User"
export class UserController {
@Get("/users")
getAll() {
return AppDataSource.manager.find(User)
}
}
{
host: "localhost", //db 접속정보
port: 3306, //db 접속정보
username: "test", //db 접속정보
password: "test", //db 접속정보
database: "test", //db 접속정보
synchronize: true, // 앱 실행 시 자동 동기화
logging: true, //쿼리 로깅 여부
entities: ["entity/*.js"], // 엔티티 경로
migrations: ["migration/*.js"] // 마이그레이션 경로
}

@Entity({ database: "secondDB" }) // user는 secondDB에 저장
export class User { ... }
@Entity({ database: "thirdDB" }) // photo는 thirdDB에 저장
export class Photo { ... }
@Entity({ schema: "secondSchema" })
export class User { ... }
@Entity({ schema: "thirdSchema" })
export class Photo { ... }
복제를 설정하면 master-slave 구조로 읽기/쓰기를 분리할 수 있다.
Replication의 경우는 DB에서도 클러스터가 설정(r/w, r/o 구성)이 세팅되어야 복제 기능 제대로 사용할 수 있다.
{
replication: {
master: { host: "server1", ... },
slaves: [
{ host: "server2", ... },
{ host: "server3", ... }
]
},
canRetry: true,
selector: "RR" // RR, RANDOM, ORDER 중 선택
}
💡 기억하기:
• Master → 쓰기 전용
• Slave → 읽기 전용
• 트래픽 분산, 안정성 향상
동작원리
const users = await dataSource.manager.find(User)
const repo = dataSource.getRepository(User)
const users = await repo.find()
// 상태 확인
dataSource.isInitialized //dataSource, 초기 연결 초기화 되었는지
dataSource.driver //dataSource의 db driver 설정
// 엔티티 매니저 사용 예시
const users = await dataSource.manager.find(User)
// 초기화, 종료, 동기화 등
await dataSource.initialize() //data source 초기화, db에 연결 pool 오픈
await dataSource.destroy() //data source 제거 및 모든 db 연결 해제
await dataSource.synchronize() //db schema 동시성
await dataSource.dropDatabase() //db drop후 모든 데이터 삭제
await dataSource.runMigrations() //migration 실행
// 레포지토리 사용
const repo = dataSource.getRepository(User)
const users = await repo.find()
// 방법 1(dataSource.transaction() 방식)
await dataSource.transaction(async (manager) => {
await manager.save(...)
await manager.update(...)
})
//📌 특징
// • 간편하고 선언적인 트랜잭션 처리
// • 내부적으로 QueryRunner를 생성, 시작, 커밋, 해제까지 자동으로 처리
// • NestJS 공식 문서에서도 일반적인 트랜잭션 처리에 가장 권장되는 방식
//✅ 장점
// • 코드가 간결하고 실수 방지에 좋음
// • 자동으로 rollback/commit 처리
// • entityManager만으로 대부분의 CRUD 처리 가능
//⚠️ 단점
// • 복잡한 쿼리 흐름, 수동 컨트롤이 필요한 경우에는 제한적
// • entityManager.query() 외의 메서드는 사용 제한이 있을 수 있음
// 방법2 (QueryRunner 방식)
const queryRunner = dataSource.createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction()
try {
await queryRunner.manager.save(...)
await queryRunner.manager.update(...)
await queryRunner.commitTransaction()
} catch (err) {
await queryRunner.rollbackTransaction()
} finally {
await queryRunner.release()
}
//📌 특징
// • 수동 트랜잭션 관리 방식
// • 더 정교한 흐름 제어 가능 (중간에 조건 분기, 세밀한 커밋 타이밍 조절 등)
// • 여러 DB 조작이 혼합된 복잡한 시나리오에서 유리
//✅ 장점
// • 유연함: 트랜잭션 범위를 더 정교하게 컨트롤 가능
// • queryRunner.query()로 raw query도 처리 가능
// • 세션 유지 등도 직접 제어 가능
//⚠️ 단점
// • 코드가 길고 복잡
// • connect, release 같은 리소스 관리 누락 시 메모리 누수 가능성 있음
// • 실수하기 쉽고 boilerplate 코드가 많음
🏆 NestJS에서는 어떤 방식이 권장될까?
NestJS + TypeORM 공식 가이드에서 기본적으로는 dataSource.transaction() 방식을 권장합니다.
• 이유는 코드 가독성, 오류율 감소, 간단한 CRUD에 충분한 기능 제공
• 다만, 아래와 같은 경우에는 QueryRunner 방식이 필요합니다:
• 하나의 트랜잭션 내에서 raw SQL을 실행해야 하는 경우
• 트랜잭션 중간에 조건 분기하여 커밋 시점을 수동으로 제어해야 할 경우
• 트랜잭션 범위를 코드 레벨에서 더 유연하게 조작해야 하는 경우
const rawData = await dataSource.query('SELECT * FROM USERS')
const users = await dataSource
.createQueryBuilder()
.select()
.from(User, "user")
.where("user.name = :name", { name: "John" })
.getMany()
const queryRunner = dataSource.createQueryRunner()
await queryRunner.connect()
// 쿼리 실행 등 작업
await queryRunner.release()
아래 원글과 참고글을 통해서 보면 더욱 도움이 될 것이다.