메인 콘텐츠로 건너뛰기
Subset은 Entity의 데이터를 다양한 형태로 조회하기 위한 미리 정의된 쿼리 템플릿입니다.

Subset 개요

유연한 조회

상황에 맞는 데이터 형식A, P, SS 등

타입 안전성

컴파일 타임 검증자동 타입 추론

관계 로딩

JOIN과 Loader 통합1:N, N:M 지원

성능 최적화

필요한 필드만 선택N+1 문제 해결

Subset이 필요한 이유

문제: 단일 쿼리의 한계

API마다 필요한 데이터 형식이 다릅니다:
// ❌ 문제 1: 목록 조회 - 너무 많은 데이터
const users = await db.table("users").selectAll();
// → 모든 컬럼을 가져옴 (password, bio, 불필요한 정보 포함)

// ❌ 문제 2: 상세 조회 - 관계 데이터 로딩 복잡
const user = await db.table("users").where("id", 1).first();
const employee = await db.table("employees").where("user_id", user.id).first();
const department = await db.table("departments").where("id", employee.department_id).first();
// → N+1 문제, 코드 중복

// ❌ 문제 3: 타입 안전성 부족
type UserList = {
  id: number;
  username: string;
  role: string;
}; // 수동으로 타입 정의 필요

해결: Subset으로 통합

// ✅ Subset A: 전체 정보
const admin = await UserModel.findById(1, ["A"]);
// 타입: { id, created_at, email, username, role, bio, ... }

// ✅ Subset P: 프로필 + 관계
const profile = await UserModel.findById(1, ["P"]);
// 타입: { id, username, employee: { salary, department: { name } } }

// ✅ Subset SS: 요약 정보
const summary = await UserModel.findById(1, ["SS"]);
// 타입: { id, username, role, last_login_at }

Subset의 구성 요소

1. SubsetQuery - 기본 쿼리

Entity에서 정의된 필드 선택:
{
  "subsets": {
    "A": ["id", "username", "email", "role"],
    "P": ["id", "username", "employee.department.name"],
    "SS": ["id", "username"]
  }
}

2. LoaderQuery - 관계 로딩

1:N, N:M 관계 데이터 로딩:
// Loader 정의 (자동 생성됨)
const loaders = {
  employees: {
    as: "employees",
    refId: "department_id",
    qb: (qb, fromIds) => 
      qb.table("employees")
        .whereIn("department_id", fromIds)
        .select({ id: "id", name: "username" }),
  },
};

3. Hydrate - 결과 변환

Flat 데이터 → 중첩 객체:
// Flat 결과 (DB에서 조회)
{
  id: 1,
  username: "john",
  employee__department__name: "Engineering"
}

// ↓ Hydrate 변환

// 중첩 객체 (최종 결과)
{
  id: 1,
  username: "john",
  employee: {
    department: {
      name: "Engineering"
    }
  }
}

Subset 명명 규칙

일반적인 Subset 이름 패턴:
Subset의미사용 예시
AAll (전체)관리자 페이지 상세
PProfile (프로필)사용자 프로필 조회
LList (목록)목록 API, 테이블 행
SSSummary (요약)드롭다운, 간단한 정보
CCard (카드)카드 UI 컴포넌트
Subset 이름은 자유롭게 정의할 수 있습니다. 팀 내에서 일관된 규칙을 사용하세요.

실전 예제

사용자 목록 API

// GET /users - 사용자 목록
async list(params: UserListParams) {
  const users = await UserModel.findMany({
    listParams: params,
    subsetKey: "L",  // List Subset 사용
  });
  
  // 타입: { id: number; username: string; role: string; created_at: Date }[]
  return users.map(user => ({
    id: user.id,
    username: user.username,
    role: user.role,
    createdAt: user.created_at,
  }));
}

사용자 프로필 API

// GET /users/:id/profile
async getProfile(userId: number) {
  const user = await UserModel.findById(userId, ["P"]);
  
  if (!user) {
    throw new Error("User not found");
  }
  
  // 타입: {
  //   id: number;
  //   username: string;
  //   employee: {
  //     salary: string;
  //     department: { name: string }
  //   }
  // }
  
  return {
    id: user.id,
    username: user.username,
    department: user.employee?.department?.name,
    salary: user.employee?.salary,
  };
}

Subset vs Raw Puri

Subset 사용 (권장)

// ✅ Subset 사용
const user = await UserModel.findById(1, ["P"]);
// - 타입 안전
// - 코드 재사용
// - 유지보수 쉬움

Raw Puri 사용

// ⚠️ Raw Puri (특수한 경우만)
const user = await UserModel.getPuri("r")
  .table("users")
  .join("employees", "users.id", "employees.user_id")
  .leftJoin("departments", "employees.department_id", "departments.id")
  .select({
    id: "users.id",
    username: "users.username",
    deptName: "departments.name",
  })
  .where("users.id", 1)
  .first();
// - 타입 추론 복잡
// - 코드 중복
// - Subset으로 표현 안 되는 특수 쿼리만 사용
언제 Raw Puri를 사용하나요?
  • Subset으로 표현할 수 없는 복잡한 쿼리
  • 성능 최적화가 필요한 특수 케이스
  • 일회성 데이터 마이그레이션
일반적인 CRUD는 항상 Subset 사용을 권장합니다.

다음 단계