PrismaORM GetPayload Generic Typing
Prisma GetPayload 기반 쿼리 결과 타입 정의 가이드
1. 문서 목적
이 문서는 Prisma Client 사용 시
Prisma.<Model>GetPayload<T> 타입을 어떻게, 왜, 어디까지 사용해야 하는지를 명확히 규정하기 위한 문서다.
특히 다음 문제를 방지하는 것을 목표로 한다.
-
Prisma 모델 타입과 쿼리 결과 타입을 혼용하는 문제
-
include / select와 실제 반환 데이터 간의 타입 불일치 -
영속성 계층 타입이 도메인까지 침투하는 설계 붕괴
2. 문제 정의: Prisma 모델 타입은 쿼리 결과 타입이 아니다
Prisma는 기본적으로 다음 두 가지를 제공한다.
import { TicketHistory } from '@prisma/client';
이 타입은 다음을 의미한다.
- 데이터베이스의 row 구조
- 관계(
relation)는 포함하지 않음 select,include에 따른 shape 변화 반영 ❌
즉, 이 타입은 “테이블 정의” 이지 쿼리 결과 가 아니다.
그럼에도 불구하고 이 타입을 Service / UseCase / Controller에서 그대로 사용하는 경우가 많고,
이 시점부터 타입 안정성은 사실상 포기 상태가 된다.
3. GetPayload<T>의 역할과 책임
3.1 정체성
Prisma.<Model>GetPayload<T>
이 타입은 다음을 의미한다.
Prisma Client가 특정 쿼리를 실행했을 때 반환하는 결과 객체의 정확한 타입
여기서 중요한 점은:
- 모델 타입이 아니다
- 엔티티 타입도 아니다
- 쿼리 명세에 종속된 결과 타입이다
3.2 제네릭 T의 의미
T는 Prisma query argument의 타입적 부분집합이다.
{
select?: ...
include?: ...
}
-
where,orderBy,take등은 타입 shape에 영향 없음 -
오직
select,include만 결과 타입을 결정한다
4. 예제 분석
export type TicketHistoryWithRecordingConsents =
Prisma.TicketHistoryGetPayload<{
include: {
recordingConsent: {
select: {
phone: true;
consent: true;
};
};
};
}>;
이 타입은 다음을 타입 레벨에서 선언한다.
TicketHistory의 기본 필드는 모두 포함recordingConsent관계를 조인- 해당 관계에서는
phone,consent필드만 노출
즉, 이 타입은 다음 Prisma 쿼리와 1:1 대응한다.
prisma.ticketHistory.findMany({
include: {
recordingConsent: {
select: { phone: true, consent: true },
},
},
});
이 대응 관계가 깨지는 순간, 타입 정의는 잘못된 것이다.
5. include와 select의 역할 분리
5.1 include
- 관계를 가져오겠다는 선언
- 관계 자체를 결과에 포함시킨다
5.2 select
- 가져온 객체의 shape를 제한
- 루트 모델과 관계 모델 양쪽에서 사용 가능
5.3 자주 발생하는 오해
include와select는 대체 관계가 아니다- 관계를 가져오려면
include가 필요하다 - 관계의 필드를 제한하려면 그 안에서
select를 사용한다
6. Nullable 관계에 대한 인식
Prisma에서 관계는 기본적으로 nullable이다.
recordingConsent: {
phone: string;
consent: boolean;
} | null
이 | null은 타입 오류가 아니라 현실 반영이다.
!로 덮는 순간, 런타임 책임은 전부 호출자에게 넘어간다- 관계 존재가 보장된다면, 쿼리 조건으로 보장해야 한다
타입은 거짓말을 하지 않는다.
거짓말을 하는 건 개발자다.
7. 사용 위치 가이드라인
7.1 사용해야 하는 곳
- Repository / Query Service
- Service 레벨의 조회 결과
- Controller DTO 변환 직전
- CQRS의 Read Model / Projection
이 타입은 **“조회 결과 명세”**로 사용한다.
7.2 사용하면 안 되는 곳
- 도메인 엔티티
- Aggregate Root
- Command / Event
- 비즈니스 규칙의 입력 타입
GetPayload는 Prisma에 종속된 타입이다.
도메인으로 올라오는 순간, 의존성 역전 원칙이 깨진다.
8. 네이밍 규칙 (강제)
다음과 같은 이름은 피한다.
TicketHistoryWithSomething
이름만 보고:
- 어떤 쿼리인지
- 어디서 쓰는지
- 재사용 가능한지
판단이 불가능하다.
권장 패턴
TicketHistoryWithRecordingConsentRow
TicketHistoryRecordingConsentProjection
TicketHistoryWithRecordingConsentForAdminQuery
**“쿼리 컨텍스트” 또는 “읽기 목적”**이 반드시 드러나야 한다.
9. 이 타입의 본질
정리하면 이 타입은:
- 재사용을 위한 타입 ❌
- 엔티티 대용 ❌
- DTO ❌
이다.
이 타입은 오직 하나다.
“이 쿼리가 반환하는 데이터의 계약서”
쿼리가 바뀌면 타입도 바뀌어야 한다.
타입이 고정되길 원한다면, 그건 이미 DTO 계층의 책임이다.
10. 결론
GetPayload는 Prisma의 가장 강력한 타입 도구다- 동시에 가장 쉽게 남용되는 도구다
- 올바르게 쓰면 타입 안정성은 올라간다
- 잘못 쓰면 설계 전체를 Prisma에 종속시킨다
타입은 편의를 위해 존재하지 않는다.
의도를 고정하기 위해 존재한다.
다음으로 이어갈 수 있는 주제는 명확하다.
GetPayload → DTO변환 레이어 분리Prisma.Args<typeof prisma.x, 'findMany'>패턴- Read Model로 승격시키는 기준
- “조회 전용 타입”과 “도메인 타입”을 구분하는 체크리스트