TypeORM Datasource
DataSource 자체는 “접속 풀(connection pool)”을 관리하는 싱글톤 객체이기 때문에, 단순히 여러 서비스에서 this.dataSource.createQueryBuilder(...)
혹은 getRepository()
를 호출한다고 해서 매번 새로운 실제 커넥션을 여는 건 아닙니다. 즉, 다음과 같은 점들을 보면 큰 문제 없이 사용할 수 있습니다.
1. DataSource는 한 번만 초기화되고, 내부적으로 커넥션 풀을 쓴다
-
NestJS(또는 일반 TypeORM) 환경에서
@InjectDataSource()
로 주입받는DataSource
인스턴스는 애플리케이션 당 하나만 생성됩니다. -
이
DataSource
는 내부적으로poolSize
(또는max
등, 사용하는 데이터베이스 드라이버마다 명칭이 다름)에 맞춰 일정 개수의 물리적 커넥션을 열어놓고 관리합니다. -
createQueryBuilder()
,getRepository()
,.find()
,.save()
같은 메서드를 호출해도, 이미 열려 있는 _풀(pool)_에서 사용 가능한 커넥션을 꺼내 쓸 뿐, 매번 새 커넥션을 팍팍 여는 게 아닙니다.
따라서 “다른 서비스에서 트랜잭션 처리용으로 DataSource를 쓰고 있는데, 여기서도 DataSource를 또 사용하면 커넥션이 너무 많아지지 않을까?” 라는 걱정은, “DataSource가 싱글톤으로 관리되고 내부에 일정 개수만큼의 커넥션 풀을 가지고 있기 때문에, 반복적으로 여러 곳에서 dataSource
를 호출해도 실제 물리 커넥션은 풀에서 재사용된다” 라고 이해하면 됩니다.
2. DataSource.transaction vs. createQueryBuilder/Repository 호출
2.1. 일반 쿼리(Repository, QueryBuilder 등)
-
this.dataSource.createQueryBuilder(...)
나this.dataSource.getRepository(SomeEntity).find(...)
같은 일반 쿼리 호출은,-
내부 풀에서 사용 가능한 커넥션 하나를 가져온다.
-
쿼리를 실행하고 결과를 받은 뒤 해당 커넥션을 풀로 반환한다.
-
이 과정이 매우 빠르기 때문에, “한 번 쿼리당 커넥션을 열었다 닫는다”가 아니라 “풀에서 꺼냈다 다시 넣는다” 수준입니다.
-
-
따라서 여러 서비스에서 순수 조회(SELECT)나 저장(INSERT/UPDATE) 로직을 작성하더라도 추가로 물리 커넥션이 계속 열리지는 않습니다. 이미 초기화된 풀(Pool) 안에서 커넥션을 재사용하기 때문입니다.
2.2. 트랜잭션(DataSource.transaction
) 호출
-
반면
this.dataSource.transaction(...)
를 호출하면, 내부적으로 새로운QueryRunner
인스턴스를 하나 생성해서 트랜잭션을 시작합니다.-
DataSource.transaction(async manager => { ... })
코드를 쓰면, TypeORM은 “새로운 QueryRunner를 만들고, 그 커넥션을 풀에서 꺼내서 BEGIN → 사용 → COMMIT/ROLLBACK → 커넥션 반환” 과정을 수행합니다. -
이때도 결국 풀에서 꺼낸 물리적 커넥션 하나를 쓰고, 트랜잭션이 끝나면 풀로 되돌려줍니다.
-
트랜잭션이 열려 있는 동안에는 동일한 QueryRunner가 보유한 커넥션이 잠금 상태이므로, 다른 쿼리는 풀의 다른 커넥션을 써야 합니다.
-
따라서 “DataSource.transaction() 을 쓰는 것” 자체가 풀에서 커넥션을 하나 점유하긴 하지만, 트랜잭션이 끝나는 시점에 반드시 풀로 반환됩니다. 만약 코드상에서
await this.dataSource.transaction(async (manager) => {
// ① manager.queryRunner (물리 커넥션 A)를 점유
// ② 여러 쿼리 실행 → 같은 커넥션 A 사용
// ③ 끝나면 커넥션 A를 풀로 반환
});
이런 흐름이라, 트랜잭션이 풀려버리면 곧바로 커넥션 A가 풀로 돌아가기 때문에, “계속 물리 커넥션이 잠기고 있어서 풀을 다 쓰는” 상황은 보통 트랜잭션이 끝나지 않았을 때 매우 긴 작업을 돌리거나, 여러 트랜잭션을 중첩해서 쓰고 있을 때나 발생하게 됩니다.
3. “여러 서비스에서 동시에 DataSource를 호출”해도 되는 이유
-
단일 DataSource 인스턴스
-
NestJS의 경우,
@InjectDataSource()
를 붙이면 전역 모듈 레벨에서 단 하나로 생성된 DataSource 인스턴스를 주입해 줍니다. -
즉, 서비스 A에 주입된
this.dataSource
와, 서비스 B에 주입된this.dataSource
는 내부적으로 동일한 객체를 참조하고, 내부 풀(pool)도 똑같이 공유합니다.
-
-
쿼리 실행 시 풀에서 커넥션을 꺼내서 사용
-
“서비스 A에서 트랜잭션용으로 DataSource.transaction을 실행 → 풀에서 커넥션 A를 꺼내 쓰고 → 트랜잭션 끝나면 반환”
-
“같은 시점에 서비스 B에서 createQueryBuilder를 실행 → 풀에서 커넥션 B(또는 빈 슬롯이 없으면 대기) 를 꺼내서 실행 → 끝나면 반환”
위 과정을 거치므로, 물리 커넥션을 불필요하게 증가시키지 않고 관리합니다.
-
-
커넥션 풀 크기 관리
-
만약 동시에 트랜잭션이 너무 많이 돌아가고, 조회/삽입 작업도 대량으로 몰리면 “풀에 미리 설정한 최대 커넥션 수(max pool size)” 이상으로 요청이 들어올 수 있습니다.
-
이때 풀에 여유 커넥션이 없으면, “대기 큐”로 들어가거나(대기열이 있다면) 에러를 던지기도 합니다.
-
따라서,
-
ormconfig.ts
또는 환경변수 등에서 적절히poolSize
(MySQL/MariaDB) 혹은max
(Postgres) 값을 설정하고, -
동시 처리량(Throughput)에 맞춰 “풀 크기를 적절히 크게” 잡아 두면 됩니다.
-
-
4. 주의해야 할 점
-
부적절하게 QueryRunner를 열고 닫지 않는 경우
-
DataSource.createQueryRunner()
를 직접 호출해서 트랜잭션 범위를 손수 관리할 때,const qr = this.dataSource.createQueryRunner(); await qr.connect(); await qr.startTransaction(); // … 여러 쿼리 await qr.commitTransaction(); // ❌ qr.release() 를 잊으면
이런 식으로
qr.release()
를 호출하지 않으면, 해당 커넥션이 풀에 반환되지 않고 “잠긴 상태”로 남아 있게 됩니다. -
따라서 만약 수동으로
createQueryRunner()
를 쓴다면, 트랜잭션 이후에 반드시release()
해 주어야 풀 고갈 문제를 방지할 수 있습니다.
-
-
Nested Transaction(중첩 트랜잭션)
-
DataSource.transaction()
내부에서 또다시DataSource.transaction()
을 호출하면, 실질적으로 “네스트된 트랜잭션”이 지원되지 않기 때문에 에러가 나거나, 의도치 않게 커넥션이 잠긴 채 남을 수 있습니다. -
여러 서비스 간에 트랜잭션을 연결해야 할 필요가 있다면, “상위 서비스에서 받은 EntityManager(또는 QueryRunner)를 그대로 하위 서비스에 넘겨주는 방식”으로 처리하는 게 좋습니다.
-
NestJS 환경이라면
@Transaction()
데코레이터나,manager
를 파라미터로 내려주는 방법으로 중첩 호출을 피하는 편이 안전합니다.
-
-
풀 크기 설정
-
정말 동시 요청(특히 트랜잭션)이 폭증하는 상황이라면, 애플리케이션 설정(
ormconfig.json
또는DataSourceOptions
)에서 적절히connectionLimit
(MySQL),max
(Postgres) 등을 조절해야 합니다. -
기본값이 너무 작아서 “동시에 N개 이상의 쿼리를 처리”해야 하는데 풀 크기가 부족하면 “대기열로 들어가거나” 또는 “커넥션 부족 오류”가 발생할 수 있습니다.
-
5. 결론
-
“다른 서비스에서 이미 DataSource를 트랜잭션 용도로 쓰고 있는데, 여기(새로운 서비스)에서도 DataSource를 쓴다면 커넥션이 불어나서 문제가 생길까?” 라는 질문에 대한 정답은:
-
DataSource는 싱글톤(공유되는 하나의 연결 풀) 이라, 단순히 여러 군데서
this.dataSource
를 호출해도 물리 커넥션이 추가로 바로 열리진 않습니다. -
풀(Pool) 내부에서 유지하는 커넥션 개수를 넘어가는 동시 작업이 몰리지 않는 한, 정상적으로 기존 풀을 재활용합니다.
-
만약 “동시 트랜잭션 + 대량 조회/삽입”이 폭증한다면, 커넥션 풀이 가득 차면서 일시적으로 대기하거나 에러가 날 수 있는데, 이건 어느 ORM을 쓰든 풀 크기를 적절히 조정해야 하는 일반적인 문제입니다.
-
-
따라서, 이미 다른 서비스(or 모듈)에서
dataSource.transaction()
같은 트랜잭션 처리를 하고 있다 하더라도, “여기서도 DataSource를 사용해서 createQueryBuilder나 getRepository를 호출”하는 건 전혀 문제가 없습니다. -
다만,
-
수동으로
createQueryRunner()
를 쓰는 부분이 있다면 반드시 release() 해 주고, -
필요한 경우
DataSourceOptions
에서 커넥션 풀 크기(max
,connectionLimit
등)를 조절해 주시면, 커넥션 부족이나 누수(leak) 없이 안정적으로 운영하실 수 있습니다.
-
결론적으로 “여러 서비스에서 동시에 DataSource를 호출해서 쿼리/트랜잭션을 처리”해도, 물리 커넥션이 과도하게 늘어나지는 않으며 큰 문제가 없습니다. 다만, 트랜잭션이 장시간 열려 있는 경우 혹은 풀 크기가 너무 작게 잡혀 있을 때는 풀 고갈 문제가 발생할 수 있으므로, 커넥션 풀 설정과 queryRunner 사용 시 release()
여부만 잘 관리해 주시면 됩니다.