TypeORM Datasource

DataSource 자체는 “접속 풀(connection pool)”을 관리하는 싱글톤 객체이기 때문에, 단순히 여러 서비스에서 this.dataSource.createQueryBuilder(...) 혹은 getRepository() 를 호출한다고 해서 매번 새로운 실제 커넥션을 여는 건 아닙니다. 즉, 다음과 같은 점들을 보면 큰 문제 없이 사용할 수 있습니다.


1. DataSource는 한 번만 초기화되고, 내부적으로 커넥션 풀을 쓴다

따라서 “다른 서비스에서 트랜잭션 처리용으로 DataSource를 쓰고 있는데, 여기서도 DataSource를 또 사용하면 커넥션이 너무 많아지지 않을까?” 라는 걱정은, “DataSource가 싱글톤으로 관리되고 내부에 일정 개수만큼의 커넥션 풀을 가지고 있기 때문에, 반복적으로 여러 곳에서 dataSource를 호출해도 실제 물리 커넥션은 풀에서 재사용된다” 라고 이해하면 됩니다.


2. DataSource.transaction vs. createQueryBuilder/Repository 호출

2.1. 일반 쿼리(Repository, QueryBuilder 등)

2.2. 트랜잭션(DataSource.transaction) 호출

따라서 “DataSource.transaction() 을 쓰는 것” 자체가 풀에서 커넥션을 하나 점유하긴 하지만, 트랜잭션이 끝나는 시점에 반드시 풀로 반환됩니다. 만약 코드상에서

await this.dataSource.transaction(async (manager) => {
  // ① manager.queryRunner (물리 커넥션 A)를 점유
  // ② 여러 쿼리 실행 → 같은 커넥션 A 사용
  // ③ 끝나면 커넥션 A를 풀로 반환
});

이런 흐름이라, 트랜잭션이 풀려버리면 곧바로 커넥션 A가 풀로 돌아가기 때문에, “계속 물리 커넥션이 잠기고 있어서 풀을 다 쓰는” 상황은 보통 트랜잭션이 끝나지 않았을 때 매우 긴 작업을 돌리거나, 여러 트랜잭션을 중첩해서 쓰고 있을 때나 발생하게 됩니다.


3. “여러 서비스에서 동시에 DataSource를 호출”해도 되는 이유

  1. 단일 DataSource 인스턴스

    • NestJS의 경우, @InjectDataSource()를 붙이면 전역 모듈 레벨에서 단 하나로 생성된 DataSource 인스턴스를 주입해 줍니다.

    • 즉, 서비스 A에 주입된 this.dataSource와, 서비스 B에 주입된 this.dataSource는 내부적으로 동일한 객체를 참조하고, 내부 풀(pool)도 똑같이 공유합니다.

  2. 쿼리 실행 시 풀에서 커넥션을 꺼내서 사용

    • “서비스 A에서 트랜잭션용으로 DataSource.transaction을 실행 → 풀에서 커넥션 A를 꺼내 쓰고 → 트랜잭션 끝나면 반환”

    • “같은 시점에 서비스 B에서 createQueryBuilder를 실행 → 풀에서 커넥션 B(또는 빈 슬롯이 없으면 대기) 를 꺼내서 실행 → 끝나면 반환”
      위 과정을 거치므로, 물리 커넥션을 불필요하게 증가시키지 않고 관리합니다.

  3. 커넥션 풀 크기 관리

    • 만약 동시에 트랜잭션이 너무 많이 돌아가고, 조회/삽입 작업도 대량으로 몰리면 “풀에 미리 설정한 최대 커넥션 수(max pool size)” 이상으로 요청이 들어올 수 있습니다.

    • 이때 풀에 여유 커넥션이 없으면, “대기 큐”로 들어가거나(대기열이 있다면) 에러를 던지기도 합니다.

    • 따라서,

      • ormconfig.ts 또는 환경변수 등에서 적절히 poolSize(MySQL/MariaDB) 혹은 max(Postgres) 값을 설정하고,

      • 동시 처리량(Throughput)에 맞춰 “풀 크기를 적절히 크게” 잡아 두면 됩니다.


4. 주의해야 할 점

  1. 부적절하게 QueryRunner를 열고 닫지 않는 경우

    • DataSource.createQueryRunner()를 직접 호출해서 트랜잭션 범위를 손수 관리할 때,

      const qr = this.dataSource.createQueryRunner();
      await qr.connect();
      await qr.startTransaction();
      // … 여러 쿼리
      await qr.commitTransaction();
      // ❌ qr.release() 를 잊으면
      

      이런 식으로 qr.release()를 호출하지 않으면, 해당 커넥션이 풀에 반환되지 않고 “잠긴 상태”로 남아 있게 됩니다.

    • 따라서 만약 수동으로 createQueryRunner()를 쓴다면, 트랜잭션 이후에 반드시 release() 해 주어야 풀 고갈 문제를 방지할 수 있습니다.

  2. Nested Transaction(중첩 트랜잭션)

    • DataSource.transaction() 내부에서 또다시 DataSource.transaction()을 호출하면, 실질적으로 “네스트된 트랜잭션”이 지원되지 않기 때문에 에러가 나거나, 의도치 않게 커넥션이 잠긴 채 남을 수 있습니다.

    • 여러 서비스 간에 트랜잭션을 연결해야 할 필요가 있다면, “상위 서비스에서 받은 EntityManager(또는 QueryRunner)를 그대로 하위 서비스에 넘겨주는 방식”으로 처리하는 게 좋습니다.

    • NestJS 환경이라면 @Transaction() 데코레이터나, manager를 파라미터로 내려주는 방법으로 중첩 호출을 피하는 편이 안전합니다.

  3. 풀 크기 설정

    • 정말 동시 요청(특히 트랜잭션)이 폭증하는 상황이라면, 애플리케이션 설정(ormconfig.json 또는 DataSourceOptions)에서 적절히 connectionLimit(MySQL), max(Postgres) 등을 조절해야 합니다.

    • 기본값이 너무 작아서 “동시에 N개 이상의 쿼리를 처리”해야 하는데 풀 크기가 부족하면 “대기열로 들어가거나” 또는 “커넥션 부족 오류”가 발생할 수 있습니다.


5. 결론

결론적으로 “여러 서비스에서 동시에 DataSource를 호출해서 쿼리/트랜잭션을 처리”해도, 물리 커넥션이 과도하게 늘어나지는 않으며 큰 문제가 없습니다. 다만, 트랜잭션이 장시간 열려 있는 경우 혹은 풀 크기가 너무 작게 잡혀 있을 때는 풀 고갈 문제가 발생할 수 있으므로, 커넥션 풀 설정과 queryRunner 사용 시 release() 여부만 잘 관리해 주시면 됩니다.