🗄️ TypeORM5. Transaction & DataSource01 — DataSource vs Connection

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

이 패턴은 다음 세 가지 가정 위에 서 있다:

  1. 단일 연결 — 앱당 하나의 Connection만 존재한다.
  2. 전역 접근어디서든 그 연결을 꺼낼 수 있다.
  3. 암묵적 라이프사이클 — 누가 만들고 닫는지가 코드 상에 없다.

이 세 가지가 멀티 테넌시 · 테스트 격리 · 멀티 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 Connection0.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 등)
  • Repository API (find, save, delete)
  • QueryBuilder API (createQueryBuilder — 단 ds.createQueryBuilder()로 호출 위치가 바뀜)
  • EntityManager API

엔티티 코드와 도메인 로직은 그대로고, 연결을 얻는 그 한 줄만 바뀐다.


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이름만 바꾼 것이 아니다.

  1. 전역 레지스트리를 제거했다 — getConnection()·getRepository()·getManager() 모두 사라졌다.
  2. 연결의 정체성을 명시적 객체로 만들었다 — 어느 연결이냐가 변수 이름으로 드러난다.
  3. 풀과 트랜잭션의 위치를 일관시켰다DataSource → QueryRunner → Transaction의 3단 위계가 코드에 그대로 나타난다.

다음 문서는 이 DataSource그 안에서 들고 있는 풀을 어떻게 설정하고 디버깅하는지 다룬다.

다음: 02 — Connection Pool — pool size, idle timeout, leak detection.