01 — DataSource vs Connection
질문: 0.3 이전의
Connection과 0.3 이후의DataSource는 왜 같지 않은가, 그리고getConnection()이 사라진 진짜 이유는 무엇인가? 한 줄 답:Connection은 전역 싱글톤처럼 다뤄지던 가변 상태였고,DataSource는 같은 객체를 명시적으로 들고 다니도록 강제하는 의존성이다. 이름이 바뀐 게 아니라, 연결을 보는 시점이 바뀌었다.
Why — 왜 이름이 바뀌었나
0.2까지 TypeORM의 Connection은 전역 레지스트리 한 곳에 박아두고 어디서든 꺼내 쓰는 객체였다.
// 0.2.x — 전역 가정
import { getConnection, getRepository } from 'typeorm';
await createConnection({ /* ... */ });
const repo = getRepository(User); // 어디서든 전역에서 꺼냄
const conn = getConnection(); // 이름 없으면 default이 패턴은 다음 세 가지 가정 위에 서 있다:
- 단일 연결 — 앱당 하나의 Connection만 존재한다.
- 전역 접근 — 어디서든 그 연결을 꺼낼 수 있다.
- 암묵적 라이프사이클 — 누가 만들고 닫는지가 코드 상에 없다.
이 세 가지가 멀티 테넌시 · 테스트 격리 · 멀티 DB · 멀티 connection이 흔해진 2020년대에 깨졌다.
0.3은 이 문제를 전역을 제거하는 것으로 풀었다.
How — DataSource의 모양
// 0.3+
import { DataSource } from 'typeorm';
export const AppDataSource = new DataSource({
type: 'postgres',
host: process.env.DB_HOST,
port: 5432,
username: 'app',
password: process.env.DB_PASSWORD,
database: 'app_prod',
entities: [User, Order, Product],
migrations: ['src/migrations/*.ts'],
synchronize: false,
});
// 앱 부트스트랩
await AppDataSource.initialize();
// 사용 — 명시적으로 ds를 들고 다닌다
const userRepo = AppDataSource.getRepository(User);
const users = await userRepo.find();
// 종료
await AppDataSource.destroy();이름이 Connection → DataSource로 바뀐 것은 수사적 결정이 아니다. DataSource라는 단어는 Java EE의 javax.sql.DataSource에서 차용됐는데, 그 단어가 의미하는 바는:
- 연결 그 자체가 아니라
- 연결을 만들어내는 정체성과 풀
즉 DataSource는 풀 + 설정 + 메타데이터의 묶음이고, 실제 연결은 QueryRunner나 트랜잭션 콜백이 빌려 쓰는 단위다.
What — 두 패러다임의 비교 매트릭스
| 항목 | 0.2.x Connection | 0.3.x DataSource |
|---|---|---|
| 이름 | Connection (단수) | DataSource (정체성 + 풀) |
| 생성 | createConnection(opts) | new DataSource(opts) → initialize() |
| 전역 접근 | getConnection() / getConnection('name') | ❌ 없음 — 변수로 직접 들고 다님 |
| Repository 얻기 | getRepository(User) (전역) | dataSource.getRepository(User) (명시) |
| 트랜잭션 | getManager().transaction(cb) | dataSource.transaction(cb) |
| 멀티 DB | 이름으로 구분 (getConnection('reports')) | 변수가 다름 (reportsDataSource) |
| 테스트 | global 상태 reset 필요 | 인스턴스를 새로 만들면 끝 |
| 닫기 | connection.close() | dataSource.destroy() |
What — 0.2 → 0.3 마이그레이션 cheat sheet
// ❌ 0.2.x
import { createConnection, getConnection, getRepository, getManager } from 'typeorm';
await createConnection({ /* opts */ });
const repo = getRepository(User);
const users = await repo.find();
await getManager().transaction(async (m) => { /* ... */ });
await getConnection().close();// ✅ 0.3.x
import { DataSource } from 'typeorm';
const ds = new DataSource({ /* opts */ });
await ds.initialize();
const repo = ds.getRepository(User);
const users = await repo.find();
await ds.transaction(async (m) => { /* ... */ });
await ds.destroy();여기서 바뀌지 않은 것은:
- 데코레이터 (
@Entity,@Column,@OneToMany등) RepositoryAPI (find,save,delete)QueryBuilderAPI (createQueryBuilder— 단ds.createQueryBuilder()로 호출 위치가 바뀜)EntityManagerAPI
즉 엔티티 코드와 도메인 로직은 그대로고, 연결을 얻는 그 한 줄만 바뀐다.
What — 멀티 DB 시나리오
// 0.3+ — 두 개의 DataSource를 *변수로* 들고 다닌다
export const ProdDB = new DataSource({
name: 'prod',
type: 'postgres',
// ...
});
export const ReportsDB = new DataSource({
name: 'reports',
type: 'mysql',
// ...
});
await Promise.all([ProdDB.initialize(), ReportsDB.initialize()]);
// 사용
const orderRepo = ProdDB.getRepository(Order);
const dailyAggRepo = ReportsDB.getRepository(DailyAggregate);NestJS에서는 @nestjs/typeorm이 이 패턴을 모듈 단위로 추상화한다.
@Module({
imports: [
TypeOrmModule.forRoot({ name: 'prod', /* ... */ }),
TypeOrmModule.forRoot({ name: 'reports', /* ... */ }),
TypeOrmModule.forFeature([Order], 'prod'),
TypeOrmModule.forFeature([DailyAggregate], 'reports'),
],
})
export class AppModule {}이게 0.2 시절의 name 인자가 살아남은 이유다 — DI 컨테이너에서 DataSource를 식별하는 키로 쓴다.
What-if — getConnection()을 여전히 쓰면
// ❌ 0.3에서 컴파일 에러
import { getConnection } from 'typeorm';
// → Module '"typeorm"' has no exported member 'getConnection'.getConnection·getRepository·getManager 전역 함수들은 전부 제거됐다. 일부 마이그레이션 가이드가 legacy 헬퍼로 그것들을 재현하는 패턴을 제시하지만 — 그건 임시 방편이고, 진짜 답은 DataSource를 의존성 주입으로 들고 다니는 것이다.
// ❌ legacy 헬퍼 (안티패턴 — 전역을 다시 만들어버림)
let _ds: DataSource;
export function getConnection() { return _ds; }
export function getRepository<T>(e: EntityTarget<T>) { return _ds.getRepository(e); }이 패턴은 0.2의 가정을 0.3 위에 다시 칠하는 짓이다 — 멀티 DB · 테스트 격리 · DI의 이점을 모두 포기한다. 마이그레이션 1단계에서만 쓰고 반드시 걷어내라.
Insight — getConnection이 사라진 진짜 이유
이름이 바뀐 게 아니다. 시점이 바뀌었다.
Connection을 어디서든 꺼내 쓴다는 가정은 연결을 결과로 본다는 시점이다 — “필요할 때 거기 있다”.DataSource를 함수 인자로 받는다는 가정은 연결의 정체성을 명시한다는 시점이다 — “어느 연결이냐”.
같은 객체를 어떻게 지칭하느냐가 시스템의 결합도를 결정한다 — 0.3은 그 지칭 방식을 강제로 바꿨다.
흥미로운 이야기 — 왜 Connection이 그렇게 오래 살아남았나
TypeORM은 2016년 시작 시점에 Hibernate(Java)의 영향을 강하게 받았는데, Hibernate에서 SessionFactory(=DataSource에 해당)와 Session(=EntityManager에 해당)이 명시적이다. 그런데 TypeORM은 그 두 개를 Connection 하나에 합쳐서 출발했다 — JavaScript 진영의 간결함을 노린 결정이었다.
결과는 두 갈래로 나타났다. 첫째, 학습 곡선이 낮아졌다. getConnection() 한 줄이면 어디서든 끝났다. 둘째, production에서 다친 사람들이 늘었다. 멀티 DB·테스트 격리·DI가 모두 그 한 줄에 막혔다.
2022년의 0.3 전환은 6년간의 학습 비용을 한 번에 회수한 결정이었다. Hibernate가 25년 전에 했던 분리를, TypeORM은 2022년에야 했다.
요약 (Pyramid Top 재정렬)
0.3의
DataSource는 0.2의Connection을 이름만 바꾼 것이 아니다.
- 전역 레지스트리를 제거했다 —
getConnection()·getRepository()·getManager()모두 사라졌다.- 연결의 정체성을 명시적 객체로 만들었다 — 어느 연결이냐가 변수 이름으로 드러난다.
- 풀과 트랜잭션의 위치를 일관시켰다 —
DataSource → QueryRunner → Transaction의 3단 위계가 코드에 그대로 나타난다.다음 문서는 이
DataSource가 그 안에서 들고 있는 풀을 어떻게 설정하고 디버깅하는지 다룬다.
다음: 02 — Connection Pool — pool size, idle timeout, leak detection.