Nest.js를 배워보자

🛠️ NestJS + Prisma에서 MyBatis처럼 XML로 SQL 소스를 관리하는 방법

_Blue_Sky_ 2025. 12. 9. 16:10
728x90

NestJS와 Prisma를 사용하면서 복잡하거나 동적인 SQL 쿼리를 다룰 때, MyBatis의 XML 맵퍼처럼 별도의 파일에서 SQL 소스를 관리하고 싶을 수 있습니다. Prisma의 raw SQL 기능을 활용하여 이와 유사한 방식으로 SQL을 분리하고 호출하는 방법을 소개합니다.


💡 핵심 아이디어: SQL 파일 분리 및 로드

Prisma는 raw SQL 쿼리를 문자열로 받기 때문에, 이 문자열을 별도의 XML 또는 .sql 파일에서 읽어와 사용하면 됩니다. XML을 파싱하는 라이브러리를 사용하거나, 단순하게 .sql 파일에 쿼리를 저장하고 파일 시스템 모듈(fs)로 불러오는 방식을 고려할 수 있습니다. 여기서는 더 간단하고 일반적인 .sql 파일을 사용하는 방법을 예시로 들어 설명합니다.


1. 📂 프로젝트 구조 설정

SQL 쿼리를 저장할 디렉토리를 만듭니다.

src/
├── module/
│   └── example/
│       ├── example.service.ts
│       └── example.controller.ts
└── resources/  <- SQL 파일을 저장할 디렉토리
    └── sql/
        └── findUserByName.sql

resources/sql/findUserByName.sql 내용:

SELECT
  id,
  name,
  email
FROM
  "User"
WHERE
  name = $1;

참고: Prisma raw SQL에서 매개변수는 보통 $1, $2 등으로 지정됩니다.


2. 📝 SQL 파일 로드 유틸리티 구현

SQL 파일을 읽어 문자열로 반환하는 유틸리티 함수를 만듭니다.

src/utils/sql-loader.util.ts:

import * as fs from 'fs';
import * as path from 'path';

/**
 * resources/sql 디렉토리에서 SQL 쿼리 파일을 읽어옵니다.
 * @param filename 쿼리 파일 이름 (예: 'findUserByName.sql')
 * @returns SQL 쿼리 문자열
 */
export const loadSqlFile = (filename: string): string => {
  // 프로젝트 루트 기준 경로
  const filePath = path.join(process.cwd(), 'src', 'resources', 'sql', filename);
  
  if (!fs.existsSync(filePath)) {
    throw new Error(`SQL file not found: ${filePath}`);
  }

  // 파일 내용을 동기적으로 읽어와 문자열로 반환
  return fs.readFileSync(filePath, 'utf8');
};

3. 🚀 NestJS 서비스에서 Raw SQL 실행

구현한 loadSqlFile 함수를 사용하여 SQL을 불러온 후, Prisma 클라이언트의 $queryRaw 또는 $executeRaw 메서드를 이용해 실행합니다.

src/module/example/example.service.ts:

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service'; // Prisma 모듈을 주입했다고 가정
import { loadSqlFile } from '../../utils/sql-loader.util';
import { Prisma } from '@prisma/client';

// 쿼리 결과 타입을 정의
interface UserQueryResult {
  id: number;
  name: string;
  email: string;
}

@Injectable()
export class ExampleService {
  constructor(private prisma: PrismaService) {}

  async findUser(userName: string): Promise<UserQueryResult[]> {
    // 1. 외부 파일에서 SQL 쿼리 로드
    const sqlQuery = loadSqlFile('findUserByName.sql');

    // 2. Prisma의 $queryRaw<T>() 메서드를 사용하여 쿼리 실행
    // 매개변수는 배열 형태로 전달하며, SQL 파일의 $1에 매핑됨.
    const users = await this.prisma.$queryRaw<UserQueryResult[]>(
      Prisma.sql`${sqlQuery}`, 
      userName
    );
    
    // 이전 Prisma 버전 또는 다른 DB 드라이버에서는 아래처럼 사용할 수도 있습니다.
    // const users = await this.prisma.$queryRaw<UserQueryResult[]>(
    //   sqlQuery, 
    //   userName
    // );
    
    return users;
  }
}

핵심 포인트: Prisma.sql 태그

Prisma의 템플릿 리터럴 태그인 Prisma.sql을 사용하면 SQL 인젝션 공격을 방지하고 매개변수를 안전하게 처리할 수 있습니다. 로드된 SQL 문자열을 이 태그 안에 넣고, 이어서 매개변수들을 전달하는 것이 가장 권장되는 방식입니다.


4. ✅ 장점 및 고려사항

구분 내용
장점 SQL 로직을 서비스 로직에서 분리하여 가독성과 유지보수성 향상
  복잡한 쿼리나 동적 쿼리 관리가 용이
고려사항 MyBatis의 강력한 동적 쿼리(if, choose 등) 기능은 직접 구현해야 함
  파일을 로드하는 과정에서 I/O 비용 발생 (초기 로딩 후 캐싱을 고려할 수 있음)
XML 대안 XML 대신 .sql 파일을 사용하면 파싱 로직 없이 단순 문자열 로드만으로 충분

 

728x90