GitHubGitHub
← 홈으로 돌아가기
backend·

PrismaModulePrismaService로 이해한 NestJS DB 연결 흐름

NestJS에서 PrismaModule과 PrismaService를 통해 DB 연결을 전역으로 관리하고, PrismaClient와 커넥션 풀을 사용하는 흐름을 정리한 글

PrismaModule과 PrismaService로 이해한 NestJS DB 연결 흐름

이전 글에서는 app.module.ts를 기준으로 NestJS 애플리케이션의 전체 모듈 구조를 정리했다.

AppModule에는 여러 기능 모듈이 등록되어 있었고, 그중 하나가 PrismaModule이었다.

이번 글에서는 PrismaModulePrismaService를 기준으로 NestJS 백엔드가 DB와 어떻게 연결되는지 정리해보려고 한다.

처음에는 Prisma를 단순히 DB 조회할 때 사용하는 도구 정도로만 생각했다.
하지만 코드를 하나씩 살펴보니, NestJS 안에서 Prisma를 안정적으로 사용하기 위해서는 모듈 등록, 의존성 주입, PrismaClient 상속, 커넥션 풀, 생명주기 관리가 함께 연결되어 있었다.

운영 중인 서비스 코드이기 때문에 실제 코드를 그대로 공개하지 않고, 핵심 흐름만 단순화해서 정리했다.


흐름 정리

이번 글에서 볼 흐름은 다음과 같다.

main.ts
NestFactory.create(AppModule)
AppModule
PrismaModule
PrismaService
PrismaClient
PrismaPg Adapter
pg Pool
PostgreSQL DB

한 문장으로 정리하면 다음과 같다.

PrismaModule은 PrismaService를 NestJS 전체에서 사용할 수 있게 등록하고,
PrismaService는 PrismaClient를 기반으로 실제 DB 연결과 쿼리 기능을 담당한다.

1. PrismaModule의 역할

먼저 PrismaModule부터 보면, 구조는 대략 다음과 같다.

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

처음에는 이 파일이 너무 짧아서 별 역할이 없어 보였다.

하지만 실제로는 중요한 역할을 한다.

PrismaService를 NestJS가 관리하는 Provider로 등록하고,
다른 모듈에서도 사용할 수 있도록 외부로 공개한다.

즉, PrismaModule은 직접 DB 쿼리를 작성하는 파일이 아니다.
DB에 접근하는 기능을 가진 PrismaService를 NestJS 애플리케이션 안에서 사용할 수 있도록 등록하는 모듈이다.


2. @Global()은 왜 사용할까?

PrismaModule에는 @Global() 데코레이터가 붙어 있다.

@Global()

@Global()은 이 모듈을 전역 모듈로 만들겠다는 의미다.

원래 NestJS에서는 어떤 모듈의 Provider를 다른 모듈에서 사용하려면, 사용하는 쪽 모듈에서 해당 모듈을 imports에 등록해야 한다.

예를 들어 UsersService에서 PrismaService를 사용하려면 원칙적으로는 UsersModule에서 PrismaModule을 import해야 한다.

@Module({
  imports: [PrismaModule],
  providers: [UsersService],
})
export class UsersModule {}

하지만 PrismaModule을 전역 모듈로 만들면, AppModule에서 한 번만 import해도 여러 모듈에서 PrismaService를 사용할 수 있다.

@Global()이 없으면
→ 각 모듈마다 PrismaModule을 import해야 한다.

@Global()이 있으면
→ AppModule에서 한 번 import한 뒤 여러 곳에서 사용할 수 있다.

PrismaService는 여러 기능에서 공통으로 사용하는 DB 접근 도구이기 때문에 전역 모듈로 두는 것이 편했다.

다만 모든 모듈을 무조건 전역으로 만드는 것은 좋은 방식이 아니다.
전역 모듈이 많아지면 어떤 모듈이 어디서 주입되는지 흐름이 흐려질 수 있기 때문이다.

DB 연결처럼 애플리케이션 전반에서 공통으로 사용하는 인프라성 모듈에 한해 전역으로 사용하는 것이 적절하다.


3. providers는 NestJS가 관리할 객체를 등록한다

PrismaModule 안에는 providers가 있다.

providers: [PrismaService]

이 코드는 NestJS에게 이렇게 알려주는 역할을 한다.

PrismaService 객체를 NestJS가 직접 생성하고 관리해줘.

이렇게 등록해두면 다른 서비스에서 직접 new PrismaService()를 하지 않아도 된다.

예를 들어 다른 서비스에서 다음처럼 생성자에 적으면 된다.

constructor(private readonly prisma: PrismaService) {}

그러면 NestJS가 PrismaService 객체를 찾아서 자동으로 넣어준다.

이게 NestJS에서 말하는 의존성 주입, 즉 DI다.

직접 객체를 만들지 않고,
NestJS가 필요한 객체를 생성해서 주입해준다.

4. exports는 다른 모듈에서도 사용할 수 있게 공개한다

providers에 등록했다고 해서 자동으로 다른 모듈에서 사용할 수 있는 것은 아니다.

다른 모듈에서도 사용하려면 exports에 공개해야 한다.

exports: [PrismaService]

이 코드는 다음과 같이 이해할 수 있다.

providers → 이 모듈 안에서 PrismaService를 사용할 수 있게 등록
exports   → 다른 모듈에서도 PrismaService를 사용할 수 있게 공개

비유하면 다음과 같다.

providers는 창고에 물건을 등록하는 것
exports는 다른 사람도 그 물건을 꺼내 쓸 수 있게 문을 열어주는 것

그래서 UsersService, AuthService, SchoolsService 같은 곳에서 PrismaService를 주입받아 사용할 수 있다.


5. PrismaService의 역할

이제 실제 DB 연결을 담당하는 PrismaService를 보자.

구조만 단순화하면 다음과 같다.

@Injectable()
export class PrismaService
  extends PrismaClient
  implements OnModuleInit, OnModuleDestroy
{
  constructor() {
    const pool = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 10,
    });

    const adapter = new PrismaPg(pool);

    super({ adapter });
  }

  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

이 코드는 크게 네 가지 역할을 한다.

1. PrismaClient 기능 상속
2. PostgreSQL 연결 Pool 생성
3. PrismaPg Adapter를 통해 PrismaClient 초기화
4. 앱 시작/종료 시 DB 연결 관리

처음 봤을 때는 코드가 짧아 보여도, 실제로는 NestJS와 Prisma, PostgreSQL 연결이 모두 만나는 중요한 파일이다.


6. @Injectable()은 주입 가능한 서비스라는 표시다

PrismaService에는 @Injectable() 데코레이터가 붙어 있다.

@Injectable()

이 데코레이터는 NestJS에게 다음과 같이 알려준다.

이 클래스는 NestJS가 직접 생성하고,
필요한 곳에 주입해줄 수 있는 클래스다.

그래서 다른 서비스에서 다음처럼 사용할 수 있다.

constructor(private readonly prisma: PrismaService) {}

만약 PrismaService가 NestJS Provider로 등록되어 있지 않다면, 이런 식으로 주입받을 수 없다.

즉, @Injectable()providers: [PrismaService]가 함께 있어야 NestJS의 의존성 주입 흐름 안에서 사용할 수 있다.


7. PrismaService extends PrismaClient

PrismaService에서 가장 중요한 부분은 이 코드였다.

export class PrismaService extends PrismaClient

extends는 상속을 의미한다.

즉, PrismaServicePrismaClient의 기능을 물려받는다는 뜻이다.

PrismaClient는 Prisma가 생성해주는 DB 클라이언트다.
DB 테이블에 접근할 수 있는 메서드들을 제공한다.

예를 들어 다른 서비스에서 다음처럼 사용할 수 있다.

this.prisma.user.findMany();
this.prisma.school.findMany();
this.prisma.matchingResult.create({
  data: {
    // 저장할 데이터
  },
});

이게 가능한 이유는 PrismaServicePrismaClient를 상속받았기 때문이다.

즉, PrismaService는 단순한 NestJS 서비스가 아니라, PrismaClient 기능을 가진 NestJS 서비스라고 볼 수 있다.

PrismaService
= NestJS가 관리하는 Service
+ PrismaClient의 DB 접근 기능

8. PrismaClient는 DB를 TypeScript 코드로 다루게 해준다

PostgreSQL에 직접 접근하려면 SQL을 작성해야 한다.

예를 들면 다음과 같다.

SELECT * FROM "User";

하지만 PrismaClient를 사용하면 TypeScript 코드로 DB에 접근할 수 있다.

this.prisma.user.findMany();

데이터 생성도 SQL을 직접 쓰는 대신 다음처럼 작성할 수 있다.

this.prisma.user.create({
  data: {
    email: 'test@example.com',
    name: '사용자',
  },
});

물론 실제 서비스에서는 사용자 데이터나 운영 데이터를 그대로 공개하면 안 되기 때문에, 블로그에서는 예시 데이터만 사용했다.

PrismaClient를 사용하면 DB 테이블을 TypeScript 코드에서 다루기 쉬워지고, 타입 도움도 받을 수 있다.


9. pg Pool은 DB 연결을 관리한다

PrismaService의 constructor 안에서는 PostgreSQL 연결 Pool을 만든다.

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
});

여기서 Pool은 DB 연결을 관리하는 객체다.

DB 연결은 비용이 있다.
요청이 올 때마다 매번 DB 연결을 새로 만들고, 쿼리를 실행한 뒤, 다시 연결을 끊는 방식은 비효율적이다.

그래서 보통 DB 연결을 Pool로 관리한다.

Pool = DB 연결을 재사용하기 위한 연결 관리자

요청이 들어왔을 때 Pool은 대략 다음과 같은 역할을 한다.

1. 사용 가능한 연결이 있는지 확인한다.
2. 있으면 그 연결을 사용한다.
3. 없고 최대 연결 수보다 적으면 새 연결을 만든다.
4. 쿼리가 끝나면 연결을 완전히 끊지 않고 다시 Pool에 반환한다.

이렇게 하면 요청마다 DB 연결을 새로 만드는 비용을 줄일 수 있다.


10. connectionString은 DB 접속 주소다

Pool을 생성할 때 connectionString을 전달한다.

connectionString: process.env.DATABASE_URL

DATABASE_URL은 DB 접속에 필요한 정보를 담고 있는 환경변수다.

실제 운영 값은 절대 공개하면 안 되기 때문에 예시로만 표현하면 다음과 같다.

DATABASE_URL=postgresql://...

이 값에는 보통 다음과 같은 정보가 들어 있다.

DB 종류
사용자명
비밀번호
호스트 주소
포트
DB 이름

중요한 점은 이 값을 코드에 직접 작성하지 않는 것이다.

DB 접속 정보는 민감한 정보이기 때문에 .env나 배포 환경변수로 관리해야 한다.


11. max: 10은 최대 연결 수를 제한한다

Pool 설정에는 max: 10도 들어간다.

max: 10

이 값은 Pool이 관리할 수 있는 DB 연결의 최대 개수를 의미한다.

예를 들어 동시에 여러 요청이 들어오면 DB 작업도 여러 개가 필요할 수 있다.

요청 A → DB 연결 1 사용
요청 B → DB 연결 2 사용
요청 C → DB 연결 3 사용

하지만 연결을 무제한으로 만들면 DB에 부담이 커질 수 있다.

그래서 최대 연결 수를 제한한다.

max: 10
= 동시에 사용할 수 있는 DB 연결 수를 최대 10개로 제한한다.

다만 이 값은 무조건 10이 정답이라는 뜻은 아니다.
서비스 트래픽, DB 사양, 배포 환경에 따라 적절한 값을 조정해야 한다.

처음에는 단순히 숫자 하나라고 생각했지만, 실제로는 서버와 DB 사이의 부하를 조절하는 중요한 설정이라고 느꼈다.


12. PrismaPg Adapter는 Prisma와 pg Pool을 연결한다

Pool을 만든 뒤에는 PrismaPg adapter를 생성한다.

const adapter = new PrismaPg(pool);

이 코드는 Prisma가 pg Pool을 사용할 수 있도록 연결해주는 역할을 한다.

흐름은 다음과 같다.

PrismaClient
PrismaPg Adapter
pg Pool
PostgreSQL

비유하면 다음과 같다.

pg Pool       → PostgreSQL 연결 관리자
PrismaPg     → Prisma가 pg Pool을 사용할 수 있게 해주는 연결 어댑터
PrismaClient → TypeScript 코드에서 DB를 다루는 클라이언트

즉, PrismaPg는 PrismaClient와 PostgreSQL 연결 Pool 사이를 이어주는 중간 계층이다.


13. super({ adapter })는 부모 클래스 초기화다

constructor의 마지막에는 super()를 호출한다.

super({ adapter });

처음에는 이 부분이 가장 헷갈렸다.

PrismaServicePrismaClient를 상속하고 있다.

export class PrismaService extends PrismaClient

TypeScript에서 자식 클래스가 부모 클래스를 상속할 때, 부모 클래스의 생성자를 실행하려면 super()를 호출해야 한다.

여기서는 부모인 PrismaClient에게 adapter를 넘겨 초기화하는 것이다.

PrismaClient야,
이 adapter를 사용해서 DB에 접근할 수 있도록 초기화해줘.

그래서 constructor 안의 흐름을 정리하면 다음과 같다.

1. pg Pool 생성
2. PrismaPg Adapter 생성
3. PrismaClient를 Adapter와 함께 초기화

이 과정을 통해 PrismaService가 실제로 DB에 접근할 준비를 하게 된다.


14. OnModuleInit으로 앱 시작 시 DB 연결 확인하기

PrismaServiceOnModuleInit을 구현한다.

async onModuleInit() {
  await this.$connect();
}

OnModuleInit은 NestJS 생명주기 인터페이스다.

생명주기라고 하면 어렵게 느껴질 수 있지만, 쉽게 말하면 다음과 같다.

앱이 켜지는 시점
앱이 동작하는 동안
앱이 종료되는 시점

onModuleInit()은 모듈이 초기화될 때 실행된다.

여기서 호출하는 $connect()는 PrismaClient가 제공하는 메서드다.

this.$connect()
= DB 연결을 시작한다.

Prisma는 첫 쿼리를 실행할 때 자동으로 연결을 시도할 수도 있다.

하지만 서버 시작 시점에 명시적으로 $connect()를 호출하면 장점이 있다.

DB 주소가 잘못되었는지
DB 비밀번호가 틀렸는지
DB에 접근할 수 없는지
서버 시작 단계에서 빠르게 확인할 수 있다.

즉, 서버는 켜졌는데 첫 API 요청에서 DB 연결 에러가 나는 상황을 줄일 수 있다.

운영 환경에서는 문제가 있다면 가능한 빨리 발견하는 것이 중요하기 때문에, 시작 시점에 연결을 확인하는 방식이 더 안전하다고 느꼈다.


15. OnModuleDestroy로 앱 종료 시 DB 연결 정리하기

반대로 서버가 종료될 때는 DB 연결을 정리해야 한다.

async onModuleDestroy() {
  await this.$disconnect();
}

$disconnect()도 PrismaClient가 제공하는 메서드다.

this.$disconnect()
= Prisma가 사용하던 DB 연결 관련 리소스를 정리한다.

서버가 종료될 때 연결을 깔끔하게 닫아두면 리소스를 더 안전하게 관리할 수 있다.

처음에는 서버가 꺼지면 연결도 알아서 사라질 거라고 생각했다.
하지만 명시적으로 종료 흐름을 관리하는 코드를 두면, 애플리케이션 생명주기를 더 명확하게 다룰 수 있다.


16. 전체 실행 흐름 다시 보기

지금까지 내용을 전체 흐름으로 정리하면 다음과 같다.

서버 시작
main.ts에서 NestFactory.create(AppModule)
AppModule이 PrismaModule import
PrismaModule이 PrismaService를 provider로 등록
NestJS가 PrismaService 객체 생성
PrismaService constructor 실행
pg Pool 생성
PrismaPg Adapter 생성
PrismaClient 초기화
onModuleInit 실행
this.$connect()로 DB 연결 확인

서버가 종료될 때는 다음과 같다.

서버 종료
onModuleDestroy 실행
this.$disconnect()로 DB 연결 정리

PrismaModulePrismaService가 단순한 DB 설정 파일이 아니라, NestJS 애플리케이션의 생명주기 안에서 DB 연결을 관리하는 구조이다.


17. 다른 서비스에서는 어떻게 사용할까?

PrismaService가 모듈에 등록되고 export되면, 다른 서비스에서는 다음처럼 주입받을 수 있다.

constructor(private readonly prisma: PrismaService) {}

그리고 서비스 안에서 PrismaClient 메서드를 사용할 수 있다.

this.prisma.user.findMany();
this.prisma.school.findMany();
this.prisma.matchingResult.create({
  data: {
    // 저장할 데이터
  },
});

이게 가능한 이유는 두 가지다.

1. PrismaService가 PrismaClient를 상속받았기 때문
2. PrismaModule이 PrismaService를 provider로 등록하고 export했기 때문

즉, 다른 서비스 입장에서는 DB 연결 방식이나 Pool 생성 과정을 직접 알 필요가 없다.

필요한 것은 주입받은 PrismaService를 사용해서 DB 작업을 수행하는 것이다.

UsersService
PrismaService
PrismaClient
DB

Controller와 Service가 역할을 나누는 것처럼, DB 연결도 PrismaService로 분리해두면 다른 서비스들은 자신의 비즈니스 로직에 집중할 수 있다.


정리

이번 글에서는 PrismaModulePrismaService를 기준으로 NestJS 백엔드가 DB와 어떻게 연결되는지 정리했다.

핵심은 다음과 같다.

PrismaModule
→ PrismaService를 NestJS 전체에서 사용할 수 있게 등록하고 공개한다.

PrismaService
→ PrismaClient를 상속해서 DB 접근 기능을 가진다.

pg Pool
→ DB 연결을 매번 새로 만들지 않고 재사용할 수 있게 관리한다.

PrismaPg Adapter
→ PrismaClient와 pg Pool 사이를 연결한다.

OnModuleInit
→ 앱 시작 시 DB 연결을 확인한다.

OnModuleDestroy
→ 앱 종료 시 DB 연결을 정리한다.

처음에는 Prisma를 단순히 this.prisma.user.findMany()처럼 DB 조회할 때 쓰는 도구로만 생각했다.

하지만 실제 구조를 보니, Prisma를 NestJS 안에서 안정적으로 사용하기 위해서는 다음 흐름이 필요했다.

모듈 등록
Provider 등록
의존성 주입
PrismaClient 상속
DB 연결 Pool 설정
앱 생명주기에 따른 연결 관리
PrismaModule과 PrismaService로 이해한 NestJS DB 연결 흐름 | OnlyMinkk Blog