개요
Prisma로 관계형 데이터베이스를 다루다 보면 자연스럽게 드는 질문이 있음 Prisma에서는 JOIN이 어디로 갔나 하는 질문임 개발자가 작성하는 Prisma Client API에는 JOIN이 없고, 서브쿼리도 보이지 않음 정말로 없는지, 없다면 왜 그런지, 어떤 트레이드오프가 있는지 정리함
ORM이란 무엇인가
ORM은 객체와 관계형 데이터베이스 간의 매핑을 제공하는 아이디어이자 구현체 집합을 의미함 애플리케이션에서 모델을 통해 데이터베이스 테이블을 간접 제어하는 추상화 계층 제공 SQL을 직접 작성하지 않고 데이터 접근 로직을 일관된 API로 수행 가능 데이터베이스 의존성 완화 효과 기대
Active Record vs Data Mapper
ORM 구현 패턴은 크게 두 가지로 요약됨
Active Record 패턴
- 클래스와 테이블의 동치에 가까운 모델링
- 필드가 컬럼, 인스턴스가 레코드에 대응
- 접근·조작 로직이 모델에 함께 존재
- 데이터베이스 스키마 변화에 코드가 민감하게 결합되는 단점
Data Mapper 패턴
- 모델과 데이터베이스를 분리하는 매핑 계층 존재
- 엔티티 클래스와 매퍼 클래스를 분리 정의
- 유연성 증가 대신 클래스 수 증가와 변화 추적 복잡도 상승 가능
일반적으로 ORM은 공통 CRUD와 쿼리 API를 제공하고, 세부 패턴은 프레임워크가 선택하거나 혼합 지원함
Prisma와 스키마 중심 모델링
Prisma는 Data Mapper 계열이지만, 자체 스키마를 중심으로 모델·마이그레이션·Client를 모두 생성하는 점이 특징임
단일 스키마 파일에서 데이터베이스 스키마와 Prisma Client가 동시에 파생됨
@map()을 통해 애플리케이션의 명명 규칙과 데이터베이스 물리명 분리 가능
예시 스키마 최소 발췌
model Notice {
id Int @id @default(autoincrement())
createdBy User @relation(fields: [createdById], references: [id])
createdById Int @map("created_by_id")
group Group @relation(fields: [groupId], references: [id])
groupId Int @map("group_id")
title String
}
Prisma Client는 이 스키마를 근거로 타입 안정성이 확보된 쿼리 API를 제공함
Prisma에는 JOIN이 없나
Prisma Client 메소드 목록에는 JOIN이 존재하지 않음 서브쿼리도 직접 제공되지 않음 그렇다면 관계형 조회는 어떻게 수행되는가가 핵심 포인트임
기본적인 include 사용 예시
await prisma.notice.findMany({
include: {
group: true,
},
})
특정 컬럼만 선택하고 싶다면 select 사용
const notices: Array<{
id: number
title: string
group: { groupName: string }
}> = await prisma.notice.findMany({
select: {
id: true,
title: true,
group: {
select: { groupName: true },
},
},
})
내부적으로 JOIN을 쓰는가
직관적으로는 JOIN 또는 서브쿼리로 변환될 것 같지만, Prisma는 관계 쿼리를 다중 SELECT로 분해해 전송함 일반적인 패턴은 부모 테이블 조회 후 자식 테이블을 IN 절로 한 번 더 조회하는 형태임
예시 로그 형태 요약
SELECT Notice.id, Notice.title, Notice.groupId FROM Notice ...
SELECT Group.id, Group.groupName FROM Group WHERE Group.id IN (...) ...
코어 엔진이 이 결과들을 클라이언트 메모리에서 합성하고, include 또는 select로 요청한 nested 구조를 만들어 반환함
스키마의 @relation 정보는 실제 데이터베이스 컬럼으로 생기지 않지만, 타입 안전성과 결과 합성에 필요한 메타로 쓰임
성능과 트레이드오프
JOIN이 없는 전략은 범용성 측면에서 장점이 있음
- 다중 데이터베이스 지원 용이
- 커넥터 구현 부담 감소
- 쿼리 엔진이 공통 로직을 재사용하기 쉬움
반면 성능 리스크 존재
- 다중 SELECT에 따른 왕복 비용 증가 가능
- 넓은 테이블 스캔 또는 부적절한 WHERE 인덱스 사용 시 비용 급증
- 대용량 데이터셋에서 JOIN 대비 비효율 가능성 높음
결론적으로 관계형 데이터베이스에서 최적화된 조인 실행 계획을 활용하지 못할 수 있음 크리티컬 경로에서는 성능 검증 필요
필요한 경우 JOIN을 쓰는 방법
ORM 순수 사용 원칙에서는 벗어나지만, Prisma에서는 raw query로 JOIN 사용 가능 핫패스나 리포트성 대용량 집계처럼 조인이 본질적인 영역에서는 고려 가치 있음 프로그래밍 모델은 Prisma Client를 유지하되 특정 쿼리만 raw로 대체하는 하이브리드 전략 권장
- 단일 책임 함수로 캡슐화
- 입력 검증과 SQL 인젝션 방지 매개 API 사용
- 스키마 변경 시 회귀 테스트로 커버
참고 문서 확인 권장
왜 처음부터 JOIN을 쓰지 않았나
역사적 배경상 Prisma의 목표는 전통적 ORM 최적화가 아니라 다중 데이터베이스를 아우르는 가상 데이터베이스 레이어에 가까웠음
- 코어 엔진에 공통 로직 집중, 커넥터는 최소한의 변환만 수행하는 구조 지향
- MongoDB처럼 JOIN이 없는 시스템도 지원해야 했음
- 따라서 쿼리를 가장 단순한 형태로 쪼개어 처리하는 전략이 합리적 선택이었음
현 시점에서는 사용자 기반이 커지며 전통적 ORM 기대치도 함께 커짐 Prisma 팀 역시 크리티컬 케이스에서 JOIN 기반 생성 지원을 추진 중이라고 알려짐 과거의 범용성 우선 전략에서, 실용적 성능과 개발 경험을 균형 있게 보완하는 과도기로 보는 편이 타당함
마무리
Prisma에는 개발자 API 차원에서 JOIN이 없음 실행 단계에서도 기본적으로 다중 SELECT 후 결과 합성 전략을 사용함 다만 실무에서는 핫패스에 한해 raw JOIN을 병행해 성능 보완 가능 장기적으로 Prisma가 선택적 JOIN 생성 옵션을 제공하면, 현재 전략과의 성능 차이를 정량적으로 검증하는 것이 합리적임 현재도 Prisma의 타입 안전성, 스키마 중심 워크플로우, 범용성은 충분히 매력적임 팀의 쿼리 특성과 트래픽 패턴을 기준으로 선택과 집중이 필요함
참고자료
- https://www.prisma.io/
- https://typeorm.io/active-record-data-mapper
- https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access
- https://github.com/prisma/prisma/issues/5184
- https://codedamn.com/news/product/dont-use-prisma
- https://www.prisma.io/day-2019
- https://www.youtube.com/watch?v=RAoXdyI_PH4