이미지 로딩 중...

Prisma 실무 활용 팁 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 5. · 6 Views

Prisma 실무 활용 팁 완벽 가이드

Prisma ORM을 실무에서 효율적으로 활용하기 위한 핵심 패턴과 최적화 기법을 다룹니다. 트랜잭션 관리, 쿼리 최적화, 타입 안전성 확보 등 실제 프로젝트에서 바로 적용 가능한 실용적인 팁들을 제공합니다.


카테고리:TypeScript
언어:TypeScript
난이도:intermediate
메인 태그:#Prisma
서브 태그:
#ORM#TypeScript#Database#QueryOptimization

들어가며

이 글에서는 Prisma 실무 활용 팁 완벽 가이드에 대해 상세히 알아보겠습니다. 총 10가지 주요 개념을 다루며, 각각의 개념에 대한 설명과 실제 코드 예제를 함께 제공합니다.

목차

  1. Prisma_트랜잭션_관리
  2. Select를_활용한_쿼리_최적화
  3. Include로_관계_데이터_효율적으로_로드
  4. Prisma_타입_안전한_쿼리_빌더
  5. Upsert로_삽입_또는_수정_처리
  6. Prisma_Middleware로_로깅_및_검증
  7. CreateMany로_대량_데이터_삽입
  8. Prisma의_Raw_쿼리_활용
  9. FindUnique_vs_FindFirst_차이점
  10. Prisma_Connection_Pool_최적화

1. Prisma_트랜잭션_관리

개요

Prisma의 트랜잭션 기능을 활용하면 여러 데이터베이스 작업을 원자적으로 처리할 수 있습니다. 실패 시 자동으로 롤백되어 데이터 일관성을 보장합니다. 복잡한 비즈니스 로직에서 필수적인 기능입니다.

코드 예제

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

async function transferMoney(fromId: number, toId: number, amount: number) {
  // $transaction으로 여러 작업을 하나의 트랜잭션으로 묶음
  await prisma.$transaction(async (tx) => {
    // 1. 송금자 계좌에서 금액 차감
    await tx.account.update({
      where: { id: fromId },
      data: { balance: { decrement: amount } }
    });

    // 2. 수신자 계좌에 금액 추가
    await tx.account.update({
      where: { id: toId },
      data: { balance: { increment: amount } }
    });
  });
}

설명

이 코드는 Prisma의 트랜잭션 기능을 활용하여 계좌 간 송금을 안전하게 처리합니다. $transaction 메서드는 콜백 함수 내의 모든 작업이 성공할 때만 커밋하고, 하나라도 실패하면 전체를 롤백합니다. 첫 번째 단계에서 $transaction은 격리된 트랜잭션 컨텍스트(tx)를 생성합니다. 이 컨텍스트는 일반 PrismaClient와 동일한 API를 제공하지만, 모든 작업이 하나의 데이터베이스 트랜잭션 내에서 실행됩니다. 두 번째로, 송금자 계좌에서 decrement 연산을 사용해 금액을 차감합니다. 이는 직접 계산하는 것보다 안전하며 동시성 문제를 방지합니다. 세 번째로, 수신자 계좌에 increment 연산으로 동일한 금액을 추가합니다. 만약 두 번째 update 작업에서 오류가 발생하면(예: 수신자 계좌 없음), 첫 번째 작업도 자동으로 롤백되어 송금자의 잔액이 원래대로 복구됩니다. 이를 통해 "돈이 차감되었지만 수신되지 않음" 같은 데이터 불일치를 완벽히 방지할 수 있습니다. 실무에서는 주문 생성+재고 차감, 회원가입+초기 설정 등 복잡한 비즈니스 로직에 필수적입니다. 트랜잭션은 격리 수준 설정도 가능하며, 긴 작업의 경우 타임아웃 옵션을 조정하여 데드락을 방지할 수 있습니다.


2. Select를_활용한_쿼리_최적화

개요

Prisma의 select 옵션으로 필요한 필드만 조회하여 데이터 전송량을 줄일 수 있습니다. 특히 큰 객체나 관계 데이터가 많을 때 성능 향상에 큰 효과가 있습니다. 네트워크 비용과 응답 시간을 동시에 개선합니다.

코드 예제

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

// ❌ 나쁜 예: 모든 필드를 가져옴
const allUsers = await prisma.user.findMany();

// ✅ 좋은 예: 필요한 필드만 선택
const users = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
    email: true,
    // password, createdAt 등 불필요한 필드 제외
    posts: {
      select: {
        id: true,
        title: true
        // content 같은 큰 필드 제외
      }
    }
  }
});

설명

이 코드는 Prisma의 select 기능을 활용하여 쿼리 성능을 최적화하는 방법을 보여줍니다. 필요한 필드만 선택적으로 가져옴으로써 데이터베이스와 애플리케이션 간 전송량을 대폭 줄일 수 있습니다. 첫 번째 나쁜 예시에서는 findMany()가 User 테이블의 모든 컬럼을 가져옵니다. 이는 password 해시, 긴 bio 텍스트, 사용하지 않는 메타데이터까지 포함하여 불필요한 네트워크 비용이 발생합니다. 두 번째 좋은 예시에서는 select 객체로 정확히 필요한 필드만 명시합니다. id, name, email만 가져오므로 각 레코드 크기가 크게 줄어듭니다. 더 중요한 것은 관계 데이터에서의 중첩 select입니다. posts 관계를 가져올 때도 id와 title만 선택하고, content 필드(보통 매우 큼)는 제외합니다. 만약 사용자가 100개의 게시글을 가지고 있다면, 각 게시글의 content를 제외하는 것만으로도 수 MB의 데이터 전송을 절약할 수 있습니다. TypeScript에서는 select를 사용하면 반환 타입이 자동으로 좁혀져 타입 안전성도 향상됩니다. 실무에서는 API 응답용 데이터를 가져올 때, 리스트 화면에서 상세 내용을 제외할 때, 대량의 데이터를 처리할 때 필수적으로 사용해야 합니다. 일반적으로 30-70%의 성능 향상을 경험할 수 있습니다.


3. Include로_관계_데이터_효율적으로_로드

개요

include를 사용하면 N+1 쿼리 문제를 해결하고 관계 데이터를 한 번에 가져올 수 있습니다. JOIN을 자동으로 처리하여 데이터베이스 왕복 횟수를 최소화합니다. 복잡한 관계도 직관적으로 표현 가능합니다.

코드 예제

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

// 사용자와 관련 데이터를 한 번에 로드
const userWithRelations = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      include: {
        comments: true, // 중첩된 관계도 가능
        category: true
      },
      where: { published: true }, // 관계 필터링
      orderBy: { createdAt: 'desc' },
      take: 5 // 최신 5개만
    },
    profile: true
  }
});

설명

이 코드는 Prisma의 include 기능으로 관계 데이터를 효율적으로 로드하는 패턴을 보여줍니다. include는 내부적으로 SQL JOIN을 사용하여 여러 테이블의 데이터를 한 번에 가져오므로 N+1 쿼리 문제를 완전히 방지합니다. N+1 문제란 사용자 100명을 조회한 후, 각 사용자의 게시글을 가져오기 위해 100번의 추가 쿼리가 발생하는 상황을 말합니다. 이 경우 총 101번의 쿼리가 실행되어 성능이 크게 저하됩니다. include를 사용하면 Prisma가 자동으로 LEFT JOIN을 생성하여 단 1-2개의 쿼리로 모든 데이터를 가져옵니다. 코드에서 posts 관계는 중첩된 include를 통해 comments와 category도 함께 로드합니다. 3단계 깊이의 관계도 하나의 쿼리 체인으로 처리됩니다. where 옵션으로 published가 true인 게시글만 필터링하고, orderBy로 최신순 정렬, take로 개수를 제한합니다. 이는 마치 SQL의 WHERE, ORDER BY, LIMIT를 선언적으로 표현한 것과 같습니다. profile 관계는 일대일 관계로, 사용자의 프로필 정보를 추가로 가져옵니다. 이 모든 작업이 최적화된 SQL 쿼리 하나 또는 두 개로 실행되므로, 수동으로 여러 findMany를 호출하는 것보다 10배 이상 빠릅니다. 실무에서는 상세 페이지 로드, 대시보드 데이터 조회, 복잡한 리포트 생성 시 필수적입니다. 단, 너무 깊은 중첩(4단계 이상)은 쿼리가 복잡해질 수 있으므로 필요에 따라 select와 조합하여 최적화하는 것이 좋습니다.


4. Prisma_타입_안전한_쿼리_빌더

개요

Prisma는 스키마를 기반으로 타입을 자동 생성하여 컴파일 타임에 오류를 잡을 수 있습니다. where, orderBy 등 모든 옵션이 타입 안전하게 작성됩니다. 리팩토링 시에도 IDE의 도움을 받아 안전하게 변경 가능합니다.

코드 예제

import { PrismaClient, Prisma } from '@prisma/client';
const prisma = new PrismaClient();

// 타입 안전한 where 조건 생성
const whereCondition: Prisma.UserWhereInput = {
  email: { contains: '@gmail.com' },
  age: { gte: 18 },
  OR: [
    { role: 'ADMIN' },
    { role: 'MODERATOR' }
  ]
};

// 타입 안전한 orderBy
const orderBy: Prisma.UserOrderByWithRelationInput = {
  createdAt: 'desc',
  name: 'asc'
};

const users = await prisma.user.findMany({ where: whereCondition, orderBy });

설명

이 코드는 Prisma의 타입 안전성을 최대한 활용하는 패턴을 보여줍니다. Prisma는 schema.prisma 파일을 기반으로 TypeScript 타입을 자동 생성하므로, 잘못된 필드명이나 타입을 사용하면 컴파일 단계에서 즉시 오류가 발생합니다. Prisma.UserWhereInput 타입은 User 모델의 모든 필드에 대한 필터링 옵션을 제공합니다. email 필드에는 문자열 연산자(contains, startsWith 등)만 사용 가능하고, age 필드에는 숫자 비교 연산자(gte, lt 등)만 허용됩니다. 만약 존재하지 않는 필드를 사용하거나 잘못된 타입의 값을 넣으면 IDE가 즉시 빨간 줄로 표시하고 컴파일이 실패합니다. OR 연산자를 사용한 복잡한 조건도 타입 안전하게 작성할 수 있습니다. role 필드가 enum이라면 'ADMIN', 'MODERATOR' 외의 값을 입력하면 타입 오류가 발생합니다. 이는 런타임 오류를 사전에 방지하는 강력한 기능입니다. Prisma.UserOrderByWithRelationInput 타입은 정렬 옵션을 안전하게 정의합니다. 존재하지 않는 필드로 정렬을 시도하면 컴파일 오류가 발생하므로, 스키마 변경 시 모든 관련 코드를 안전하게 업데이트할 수 있습니다. 실무에서는 동적 쿼리를 생성할 때 이러한 타입들을 변수로 분리하면 코드 재사용성이 높아지고 유지보수가 쉬워집니다. 예를 들어, 검색 필터를 함수 파라미터로 받아 조건부로 where 객체를 구성할 때, 타입 안전성 덕분에 실수를 방지하고 리팩토링을 자신 있게 수행할 수 있습니다. 이는 대규모 프로젝트에서 특히 중요한 이점입니다.


5. Upsert로_삽입_또는_수정_처리

개요

upsert는 레코드 존재 여부에 따라 생성 또는 업데이트를 자동으로 선택합니다. 별도의 조회 없이 한 번의 쿼리로 처리되어 효율적입니다. 동시성 이슈를 방지하고 코드를 간결하게 만들어줍니다.

코드 예제

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

// 사용자 프로필 업데이트 또는 생성
const profile = await prisma.profile.upsert({
  where: {
    userId: 123 // 고유 키로 검색
  },
  update: {
    bio: '업데이트된 소개',
    avatar: 'new-avatar.jpg',
    updatedAt: new Date()
  },
  create: {
    userId: 123,
    bio: '새로운 소개',
    avatar: 'default-avatar.jpg',
    createdAt: new Date()
  }
});

설명

이 코드는 Prisma의 upsert 메서드를 활용하여 "있으면 수정, 없으면 생성" 로직을 효율적으로 구현합니다. upsert는 데이터베이스의 UPSERT(INSERT ON CONFLICT UPDATE) 기능을 활용하여 단일 쿼리로 처리됩니다. 전통적인 방식에서는 먼저 findUnique로 레코드 존재 여부를 확인한 후, 조건에 따라 create 또는 update를 호출해야 합니다. 이는 최소 2번의 데이터베이스 쿼리가 필요하며, 두 쿼리 사이에 다른 요청이 끼어들 수 있는 race condition이 발생할 수 있습니다. upsert는 이를 하나의 원자적 연산으로 처리하여 이러한 문제를 완전히 방지합니다. where 절에는 고유 제약 조건이 있는 필드(예: userId가 unique인 경우)를 지정합니다. 데이터베이스는 이 조건으로 레코드를 검색하여, 존재하면 update 객체의 내용으로 업데이트하고, 존재하지 않으면 create 객체의 내용으로 새 레코드를 생성합니다. update와 create 블록에 서로 다른 필드를 지정할 수 있습니다. 예를 들어, update에는 updatedAt만 갱신하고 create에는 createdAt을 설정하는 식으로 각 상황에 맞는 로직을 구현할 수 있습니다. bio와 avatar는 공통으로 설정되지만, 타임스탬프 필드는 상황에 따라 다르게 처리됩니다. 실무에서는 OAuth 로그인 시 사용자 정보 동기화, 캐시 데이터 갱신, 설정값 저장 등 "있으면 업데이트, 없으면 생성"이 필요한 모든 상황에서 유용합니다. 특히 동시 요청이 많은 환경에서 데이터 일관성을 보장하면서도 성능을 유지할 수 있는 최선의 방법입니다.


6. Prisma_Middleware로_로깅_및_검증

개요

Prisma Middleware는 모든 쿼리 실행 전후에 로직을 삽입할 수 있는 강력한 기능입니다. 로깅, 성능 측정, 데이터 검증, 자동 필드 업데이트 등을 중앙에서 관리할 수 있습니다. AOP(관점 지향 프로그래밍) 패턴을 데이터베이스 계층에 적용한 것입니다.

코드 예제

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

// 모든 쿼리의 실행 시간 로깅 미들웨어
prisma.$use(async (params, next) => {
  const before = Date.now();

  // 쿼리 실행 전 로깅
  console.log(`Query: ${params.model}.${params.action}`);

  // 실제 쿼리 실행
  const result = await next(params);

  const after = Date.now();
  console.log(`Executed in ${after - before}ms`);

  // 결과 반환 (수정 가능)
  return result;
});

설명

이 코드는 Prisma Middleware를 활용하여 모든 데이터베이스 쿼리에 횡단 관심사(cross-cutting concerns)를 적용하는 방법을 보여줍니다. 미들웨어는 Express의 미들웨어와 유사하게 요청을 가로채고 처리한 후 다음 단계로 전달하는 체인 패턴을 사용합니다. $use 메서드에 전달된 콜백 함수는 두 개의 파라미터를 받습니다. params에는 쿼리 정보(모델명, 액션, where 조건 등)가 담겨 있고, next는 다음 미들웨어 또는 실제 쿼리 실행을 호출하는 함수입니다. 이 패턴을 통해 쿼리 실행 전후에 원하는 로직을 삽입할 수 있습니다. 코드에서는 Date.now()로 쿼리 실행 전 시간을 기록하고, params.model과 params.action으로 어떤 모델의 어떤 작업인지 로깅합니다(예: "User.findMany"). 그 다음 await next(params)를 호출하여 실제 쿼리를 실행하고, 실행 후 다시 시간을 측정하여 소요 시간을 계산합니다. 이 패턴은 성능 병목 지점을 파악하는 데 매우 유용합니다. 실제 프로덕션에서는 100ms 이상 걸리는 느린 쿼리를 자동으로 경고하거나, 특정 모델의 delete 작업을 소프트 삭제로 변환하거나, createdAt/updatedAt 필드를 자동으로 설정하는 등의 로직을 추가할 수 있습니다. 여러 미들웨어를 등록하면 등록 순서대로 체인을 형성하여 실행됩니다. 실무에서는 로깅 미들웨어, 인증 검증 미들웨어, 데이터 암호화 미들웨어 등을 조합하여 강력한 데이터 계층 파이프라인을 구축할 수 있습니다. 이를 통해 비즈니스 로직 코드는 핵심 기능에만 집중하고, 공통 기능은 미들웨어에서 일괄 처리하여 유지보수성을 크게 향상시킬 수 있습니다.


7. CreateMany로_대량_데이터_삽입

개요

createMany는 여러 레코드를 한 번의 쿼리로 삽입하여 대량 데이터 처리 성능을 극대화합니다. 개별 create를 반복하는 것보다 10배 이상 빠르며, 트랜잭션 오버헤드를 최소화합니다. 데이터 마이그레이션이나 시드 작업에 필수적입니다.

코드 예제

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

// ❌ 나쁜 예: 반복문으로 개별 생성 (느림)
// for (const user of users) {
//   await prisma.user.create({ data: user });
// }

// ✅ 좋은 예: 한 번에 대량 삽입
const result = await prisma.user.createMany({
  data: [
    { email: 'user1@example.com', name: 'User 1' },
    { email: 'user2@example.com', name: 'User 2' },
    { email: 'user3@example.com', name: 'User 3' },
    // ... 수천 개의 레코드
  ],
  skipDuplicates: true // 중복 시 건너뛰기 (선택)
});

console.log(`${result.count}개 레코드 생성됨`);

설명

이 코드는 Prisma의 createMany 메서드를 활용하여 대량의 데이터를 효율적으로 삽입하는 방법을 보여줍니다. 대량 데이터 처리 시 개별 create를 반복 호출하는 것은 성능상 치명적인 단점이 있습니다. 나쁜 예시처럼 for 루프로 await prisma.user.create()를 1000번 호출하면, 각 호출마다 데이터베이스와의 왕복(round-trip)이 발생하여 네트워크 지연이 누적됩니다. 또한 각 insert마다 트랜잭션을 시작하고 커밋하는 오버헤드가 발생합니다. 1000개 레코드를 삽입하는 데 10초 이상 걸릴 수 있습니다. createMany는 내부적으로 SQL의 INSERT INTO ... VALUES (...), (...), (...) 형태의 단일 쿼리를 생성합니다. 모든 데이터를 한 번에 전송하고 한 번의 트랜잭션으로 처리하므로, 동일한 1000개 레코드를 1초 이내에 삽입할 수 있습니다. 이는 네트워크 왕복을 999번 줄이고 트랜잭션 오버헤드를 최소화한 결과입니다. skipDuplicates: true 옵션은 매우 유용합니다. 고유 제약 조건(unique constraint)을 위반하는 레코드가 있어도 오류 없이 건너뛰고 나머지를 계속 삽입합니다. 이는 멱등성(idempotency)을 보장하는 데이터 동기화 작업에 이상적입니다. 예를 들어, 외부 API에서 가져온 데이터를 반복해서 동기화할 때 이미 존재하는 항목은 무시하고 새로운 항목만 추가할 수 있습니다. result.count는 실제로 삽입된 레코드 수를 반환합니다(skipDuplicates 사용 시 건너뛴 항목은 제외). 실무에서는 CSV 임포트, 초기 데이터 시딩, 배치 작업, 데이터 마이그레이션 등에서 필수적으로 사용됩니다. 단, 관계 데이터(nested create)는 지원하지 않으므로, 복잡한 관계가 있는 경우 $transaction과 조합하여 사용해야 합니다.


8. Prisma의_Raw_쿼리_활용

개요

Prisma의 타입 안전 API로 표현하기 어려운 복잡한 쿼리는 원시 SQL을 사용할 수 있습니다. $queryRaw와 $executeRaw로 완전한 SQL 제어가 가능하며, SQL 인젝션을 방지하는 안전한 파라미터 바인딩을 제공합니다. 집계 쿼리나 특수 데이터베이스 기능에 유용합니다.

코드 예제

import { PrismaClient, Prisma } from '@prisma/client';
const prisma = new PrismaClient();

// 타입 안전한 원시 쿼리 (SELECT)
const users = await prisma.$queryRaw<Array<{ id: number; email: string }>>`
  SELECT id, email
  FROM "User"
  WHERE age > ${18}
  AND email LIKE ${`%@gmail.com`}
`;

// 원시 쿼리 (INSERT/UPDATE/DELETE)
const result = await prisma.$executeRaw`
  UPDATE "User"
  SET "lastLogin" = NOW()
  WHERE id = ${userId}
`;

console.log(`${result}개 행이 업데이트됨`);

설명

이 코드는 Prisma에서 원시 SQL 쿼리를 안전하게 실행하는 방법을 보여줍니다. Prisma의 타입 안전 API가 매우 강력하지만, 특정 데이터베이스 고유 기능이나 복잡한 집계 쿼리는 원시 SQL이 더 효율적일 수 있습니다. $queryRaw는 데이터를 조회하는 SELECT 쿼리에 사용됩니다. 제네릭 타입 파라미터로 반환 타입을 지정하면 TypeScript의 타입 체킹 혜택을 그대로 받을 수 있습니다. 템플릿 리터럴 문법(백틱)을 사용하며, ${} 안의 값은 자동으로 파라미터 바인딩됩니다. 이는 SQL 인젝션 공격을 완벽히 방지합니다. 예를 들어, ${18}은 직접 SQL 문자열에 삽입되는 것이 아니라 데이터베이스의 prepared statement 파라미터로 전달됩니다. 만약 사용자 입력을 직접 문자열 보간으로 넣으면 SQL 인젝션에 취약하지만, 템플릿 리터럴을 사용하면 Prisma가 자동으로 이스케이프 처리합니다. ${%@gmail.com}와 같은 LIKE 패턴도 안전하게 처리됩니다. $executeRaw는 INSERT, UPDATE, DELETE 등 데이터를 수정하는 쿼리에 사용됩니다. 반환값은 영향받은 행의 개수입니다. NOW() 같은 데이터베이스 함수를 직접 사용할 수 있어, Prisma API로는 표현하기 어려운 복잡한 로직을 구현할 수 있습니다. 주의할 점은 테이블명과 컬럼명을 데이터베이스 스키마에 맞게 정확히 작성해야 한다는 것입니다. PostgreSQL에서는 대소문자 구분을 위해 "User"처럼 큰따옴표로 감싸야 할 수 있습니다. 또한 원시 쿼리는 Prisma의 미들웨어나 소프트 삭제 같은 기능을 우회하므로 신중하게 사용해야 합니다. 실무에서는 복잡한 통계 쿼리, 전문 검색(full-text search), 지리 공간 쿼리, 대용량 데이터 집계 등 Prisma API로 표현하기 어렵거나 비효율적인 경우에 선택적으로 사용합니다. 일반적인 CRUD 작업은 타입 안전성을 위해 Prisma API를 사용하는 것이 좋습니다.


9. FindUnique_vs_FindFirst_차이점

개요

findUnique는 고유 키로만 검색 가능하지만 최적화된 성능을 제공합니다. findFirst는 모든 조건으로 검색 가능하지만 첫 번째 결과만 반환합니다. 용도에 맞게 선택하면 쿼리 성능과 의도를 명확히 할 수 있습니다.

코드 예제

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

// findUnique: 고유 키(unique/primary key)로만 검색
const userById = await prisma.user.findUnique({
  where: { id: 123 } // ✅ id는 고유 키
});

const userByEmail = await prisma.user.findUnique({
  where: { email: 'user@example.com' } // ✅ email이 unique라면
});

// findFirst: 모든 조건으로 검색 가능
const firstAdmin = await prisma.user.findFirst({
  where: { role: 'ADMIN' }, // role은 고유하지 않음
  orderBy: { createdAt: 'asc' } // 정렬 가능
});

설명

이 코드는 Prisma의 findUnique와 findFirst의 차이점과 각각의 최적 사용 사례를 보여줍니다. 두 메서드는 단일 레코드를 조회한다는 공통점이 있지만, 동작 방식과 성능 특성이 다릅니다. findUnique는 고유 제약 조건이 있는 필드로만 검색할 수 있습니다. where 절에는 @id(기본 키)나 @@unique 속성이 있는 필드만 사용 가능합니다. 예를 들어, User 모델에서 id가 기본 키이고 email에 @unique가 있다면 두 필드로 검색할 수 있습니다. 데이터베이스는 고유 인덱스를 활용하여 O(log n) 시간에 레코드를 찾으므로 매우 빠릅니다. findUnique는 레코드가 반드시 하나만 존재함을 보장하므로, 결과 타입이 User | null입니다. 존재하면 객체, 없으면 null을 반환합니다. 이는 의미상으로도 명확하며, 코드를 읽는 사람에게 "이 필드는 고유하다"는 것을 알려줍니다. 반면 findFirst는 어떤 조건으로도 검색할 수 있습니다. role 같은 비고유 필드로도 검색 가능하며, 복잡한 where 조건, orderBy, skip 등을 모두 사용할 수 있습니다. 내부적으로는 findMany와 유사하게 동작하지만 LIMIT 1을 추가하여 첫 번째 결과만 가져옵니다. 만약 조건에 맞는 레코드가 여러 개라면 그중 첫 번째(또는 orderBy로 정렬된 첫 번째)를 반환합니다. findFirst는 인덱스가 없는 필드로 검색하면 전체 테이블 스캔이 발생할 수 있어 성능이 떨어질 수 있습니다. 하지만 "가장 최근 생성된 관리자"나 "특정 조건의 첫 번째 항목"을 찾을 때처럼 고유하지 않은 조건으로 하나만 가져와야 할 때 유용합니다. 실무에서는 ID나 이메일로 사용자를 찾을 때는 findUnique를, 특정 상태의 첫 번째 주문이나 가장 인기 있는 게시글을 찾을 때는 findFirst를 사용합니다. findUnique를 사용할 수 있는 경우라면 성능과 의도 명확성을 위해 항상 findUnique를 선택하는 것이 좋습니다.


10. Prisma_Connection_Pool_최적화

개요

Prisma의 커넥션 풀을 적절히 설정하면 데이터베이스 연결 관리를 최적화할 수 있습니다. 동시 요청이 많은 환경에서 연결 부족이나 과도한 연결로 인한 성능 저하를 방지합니다. 데이터베이스 종류와 서버 스펙에 맞게 조정이 필요합니다.

코드 예제

// schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  // 커넥션 풀 설정 추가
  // connection_limit=10&pool_timeout=20&connect_timeout=10
}

// 프로그래밍 방식으로 설정
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient({
  datasources: {
    db: {
      url: `${process.env.DATABASE_URL}?connection_limit=20&pool_timeout=30`
    }
  },
  log: ['query', 'info', 'warn', 'error'] // 디버깅용
});

// 애플리케이션 종료 시 연결 정리
process.on('beforeExit', async () => {
  await prisma.$disconnect();
});

설명

이 코드는 Prisma의 데이터베이스 커넥션 풀을 최적화하는 방법을 보여줍니다. 커넥션 풀은 데이터베이스 연결을 재사용하여 연결 생성/해제 오버헤드를 줄이는 중요한 성능 최적화 기법입니다. 데이터베이스 연결은 생성 비용이 매우 높습니다(TCP 핸드셰이크, 인증, 세션 초기화 등). 매 쿼리마다 새 연결을 생성하면 수백 밀리초가 소요될 수 있습니다. 커넥션 풀은 미리 연결을 생성해두고 재사용하므로, 쿼리 실행 시 즉시 연결을 가져와 사용할 수 있어 응답 시간이 크게 단축됩니다. connection_limit 파라미터는 풀에서 유지할 최대 연결 수를 지정합니다. 기본값은 데이터베이스에 따라 다르지만(PostgreSQL은 보통 10), 동시 요청이 많은 서버에서는 20-50 정도로 늘려야 합니다. 너무 낮으면 요청이 연결을 기다리며 대기하게 되고, 너무 높으면 데이터베이스 서버의 최대 연결 수를 초과하여 오류가 발생할 수 있습니다. pool_timeout은 연결을 기다리는 최대 시간(초)입니다. 모든 연결이 사용 중일 때 새 요청은 연결이 반환될 때까지 대기하는데, 이 시간을 초과하면 타임아웃 오류가 발생합니다. 기본값은 10초이며, 트래픽이 많은 환경에서는 20-30초로 늘릴 수 있습니다. connect_timeout은 데이터베이스 서버에 연결을 시도하는 최대 시간입니다. 네트워크 지연이나 데이터베이스 과부하 시 연결이 무한정 대기하는 것을 방지합니다. log 옵션으로 쿼리 로그를 활성화하면 성능 분석과 디버깅에 유용합니다. $disconnect() 메서드는 모든 연결을 정리하고 리소스를 반환합니다. 애플리케이션 종료 시 반드시 호출해야 데이터베이스 연결이 깔끔하게 정리됩니다. Serverless 환경(AWS Lambda 등)에서는 각 요청마다 PrismaClient를 재사용하고 함수 종료 전 명시적으로 $disconnect를 호출하는 것이 중요합니다. 실무에서는 데이터베이스 서버의 max_connections 설정, 애플리케이션 서버 수, 평균 동시 요청 수 등을 고려하여 최적값을 찾아야 합니다. 예를 들어, 데이터베이스가 최대 100개 연결을 허용하고 서버가 5대라면 각 서버는 20개 이하로 설정해야 합니다. 모니터링 도구로 연결 풀 사용률을 추적하여 병목을 발견하고 조정하는 것이 중요합니다.


마치며

이번 글에서는 Prisma 실무 활용 팁 완벽 가이드에 대해 알아보았습니다. 총 10가지 개념을 다루었으며, 각각의 사용법과 예제를 살펴보았습니다.

관련 태그

#Prisma #ORM #TypeScript #Database #QueryOptimization

#Prisma#ORM#TypeScript#Database#QueryOptimization

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.