> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Subset 사용하기

> Model에서 Subset 활용하기

Subset은 Model의 메서드를 통해 사용합니다.

## Model 메서드와 Subset

### findById - 단일 레코드 조회

```typescript theme={null}
// Subset 1개
const user = await UserModel.findById(1, ["P"]);
// 타입: UserSubsetMapping["P"]

// Subset 여러 개 (Union)
const user = await UserModel.findById(1, ["A", "P"]);
// 타입: UserSubsetMapping["A"] | UserSubsetMapping["P"]
```

### findOne - 조건으로 단일 레코드

```typescript theme={null}
const user = await UserModel.findOne({
  where: [["email", "john@test.com"]],
  subsetKey: "P",
});
// 타입: UserSubsetMapping["P"] | undefined
```

### findMany - 여러 레코드 조회

```typescript theme={null}
const users = await UserModel.findMany({
  listParams: {
    page: 1,
    pageSize: 20,
  },
  subsetKey: "L",
});
// 타입: UserSubsetMapping["L"][]
```

## getSubsetQueries로 Subset 쿼리 시작

`getSubsetQueries`를 사용하여 Subset 기반 쿼리를 시작합니다:

```typescript theme={null}
// Subset P의 쿼리 빌더 획득
const { qb, onSubset } = UserModel.getSubsetQueries("P");

// 추가 조건 적용
qb.where("users.role", "admin")
  .where("users.is_verified", true)
  .orderBy("users.created_at", "desc");

// executeSubsetQuery로 실행
const result = await UserModel.executeSubsetQuery({
  subset: "P",
  qb,
  params: { num: 20, page: 1 },
});
// 타입: ListResult<{ num: number; page: number }, UserSubsetMapping["P"]>
```

<Info>
  `getSubsetQueries("P")`는 Subset P에 정의된 JOIN과 SELECT를 자동으로 적용한 쿼리 빌더(`qb`)를
  반환합니다.
</Info>

## Subset 쿼리 확장

### WHERE 조건 추가

```typescript theme={null}
const { qb } = UserModel.getSubsetQueries("L");

qb.where("users.role", "admin").where("users.is_verified", true);

const result = await UserModel.executeSubsetQuery({
  subset: "L",
  qb,
  params: { num: 20, page: 1 },
});
// Subset L의 기본 필드 + WHERE 조건
```

### ORDER BY 정렬

```typescript theme={null}
const { qb } = UserModel.getSubsetQueries("L");

qb.orderBy("users.created_at", "desc");

const result = await UserModel.executeSubsetQuery({
  subset: "L",
  qb,
  params: { num: 10, page: 1 },
});
```

### 복합 조건

```typescript theme={null}
const { qb, onSubset } = UserModel.getSubsetQueries("P");

qb.where("users.role", "normal").orderBy("employee.salary", "desc");

// onSubset으로 특정 Subset의 테이블에 접근
onSubset("P").where("employee__department.name", "Engineering");
onSubset("P").where("employee.salary", ">", "60000");

// Subset P는 employee.department.name을 포함하므로
// JOIN이 자동으로 설정됨
```

## Subset 타입 안전성

### 컴파일 타임 검증

```typescript theme={null}
// ✅ OK: Subset P는 employee__department 테이블 포함
const { qb, onSubset } = UserModel.getSubsetQueries("P");
onSubset("P").where("employee__department.name", "Engineering");

// ❌ Type Error: Subset L은 employee 테이블 미포함
const { qb: qbL, onSubset: onSubsetL } = UserModel.getSubsetQueries("L");
onSubsetL("L").where("employee__department.name", "Engineering");
//             ^^^^^^^^^^^^^^^^^^^^^^^^^ Property does not exist
```

### 자동 타입 추론

```typescript theme={null}
const user = await UserModel.findById(1, ["P"]);

// TypeScript가 자동으로 타입 추론
user.id; // number
user.username; // string
user.employee; // { salary: string; department: { name: string } } | null

// ❌ Compile Error
user.password; // Property 'password' does not exist
```

### Subset 교집합 (Multiple Subsets)

여러 Subset을 동시에 지정하면, TypeScript는 **교집합 타입**을 자동으로 계산합니다.

#### 기본 교집합

```typescript theme={null}
// Subset A와 P의 교집합
const user = await UserModel.findById(1, ["A", "P"]);

// 타입: 두 Subset에 공통으로 있는 필드만 사용 가능
user.id; // ✅ OK (A, P 모두 포함)
user.username; // ✅ OK (A, P 모두 포함)
user.email; // ✅ OK (A, P 모두 포함)

user.employee; // ❌ Type Error (A에는 없음, P에만 있음)
user.bio; // ❌ Type Error (P에는 없음, A에만 있음)
```

**교집합이 동작하는 이유**:

* `["A", "P"]`는 Union이 아닌 **Intersection** 타입을 생성
* 런타임에는 두 Subset 중 **더 큰 Subset**의 데이터가 반환됨
* 하지만 타입 시스템은 **공통 필드만 보장**

#### 쿼리에서의 교집합

```typescript theme={null}
// 쿼리 시작: A Subset 기반
const { qb, onSubset } = UserModel.getSubsetQueries("A");

// ✅ OK: 공통 필드만 WHERE 조건 가능
qb.where("users.username", "like", "%john%");
qb.where("users.email", "john@example.com");

// onSubset으로 여러 Subset의 교집합 타입 사용
onSubset(["A", "P"]).where("users.id", 1);

// ❌ Type Error: employee는 A와 P 교집합에 없음 (A에 없음)
// onSubset(["A", "P"]).where("employee__department.name", "Engineering");
//                      ^^^^^^^^^^^^^^^^^^^^^^^^^ Property does not exist
```

#### 실전 예제: 조건부 Subset 선택

```typescript theme={null}
async getUserData(userId: number, includeEmployeeInfo: boolean) {
  // 조건에 따라 Subset 선택
  const subsets = includeEmployeeInfo ? ["P"] : ["A"];

  const user = await UserModel.findById(userId, subsets);

  // TypeScript 타입:
  // - includeEmployeeInfo가 true면: UserSubsetMapping["P"]
  // - includeEmployeeInfo가 false면: UserSubsetMapping["A"]
  // - 하지만 변수 subsets의 타입이 ["P"] | ["A"]이므로
  //   실제로는 교집합 타입이 추론됨

  return {
    id: user.id,
    username: user.username,
    email: user.email,
    // employee 필드는 타입 체크 통과 안됨 (교집합에 없음)
  };
}
```

**교집합 우회 방법**:

```typescript theme={null}
// 방법 1: 타입 가드 사용
async getUserDataWithGuard(userId: number, includeEmployeeInfo: boolean) {
  if (includeEmployeeInfo) {
    const user = await UserModel.findById(userId, ["P"]);
    // 여기서 user는 UserSubsetMapping["P"] 타입
    return {
      ...user,
      department: user.employee?.department?.name,
    };
  } else {
    const user = await UserModel.findById(userId, ["A"]);
    // 여기서 user는 UserSubsetMapping["A"] 타입
    return user;
  }
}

// 방법 2: 제네릭 사용
async getUserDataGeneric<T extends UserSubsetKey>(
  userId: number,
  subset: T
): Promise<UserSubsetMapping[T]> {
  const user = await UserModel.findById(userId, [subset]);
  return user;
}

// 사용
const userA = await getUserDataGeneric(1, "A");  // 타입: UserSubsetMapping["A"]
const userP = await getUserDataGeneric(1, "P");  // 타입: UserSubsetMapping["P"]
```

#### onSubset 메서드와 교집합

`onSubset` 메서드로 Subset 조건을 동적으로 적용할 때도 교집합이 계산됩니다:

```typescript theme={null}
const { qb, onSubset } = UserModel.getSubsetQueries("A");

// Subset A와 P 조건 모두 적용 (교집합 타입)
onSubset(["A", "P"]).where("users.username", "like", "%john%"); // ✅ OK

// ❌ Type Error: employee는 교집합에 없음
// onSubset(["A", "P"]).where("employee__department.name", "Engineering");
```

**onSubset의 타입 추론**:

```typescript theme={null}
const { qb, onSubset } = UserModel.getSubsetQueries("A");

// 단일 Subset
onSubset("A").where("users.id", 1);
// qb는 Subset A의 모든 필드 사용 가능

// 복수 Subset (교집합)
onSubset(["A", "P"]).where("users.id", 1);
// qb는 A와 P의 교집합 필드만 사용 가능

// 교집합이 비어있으면?
onSubset(["L", "P"]).where("users.id", 1);
// L과 P의 공통 필드만 사용 가능
// 만약 공통 필드가 거의 없다면, 사용 가능한 필드가 매우 제한적
```

<Warning>
  **교집합 사용 시 주의사항**: 1. **런타임 데이터 ≠ 타입**: 런타임에는 더 큰 Subset의 모든 필드가
  반환되지만, 타입 시스템은 교집합만 보장 2. **의도하지 않은 교집합**: 여러 Subset을 배열로 전달할
  때 실수로 교집합이 발생할 수 있음 3. **타입 가드 권장**: 조건부로 Subset을 선택할 때는 타입 가드를
  사용하여 정확한 타입 추론
</Warning>

<Info>
  **언제 교집합을 사용하나요?**

  * 여러 API에서 공통으로 필요한 필드만 조회할 때
  * 타입 안전성을 극대화하고 싶을 때
  * 불필요한 필드 노출을 방지하고 싶을 때

  대부분의 경우 **단일 Subset** 사용을 권장합니다.
</Info>

## 실전 예제

### 사용자 목록 API

```typescript theme={null}
// Model 내부 메서드 (권장 패턴)
class UserModelClass extends BaseModelClass<...> {
  @api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
  async findMany<T extends UserSubsetKey>(
    subset: T,
    rawParams?: UserListParams
  ): Promise<ListResult<UserListParams, UserSubsetMapping[T]>> {
    const params = { num: 24, page: 1, ...rawParams };
    const { qb, onSubset } = this.getSubsetQueries(subset);

    // 역할 필터
    if (params.role) {
      qb.where("users.role", params.role);
    }

    // 검색
    if (params.keyword) {
      qb.where("users.username", "like", `%${params.keyword}%`);
    }

    // 정렬
    qb.orderBy("users.created_at", "desc");

    return this.executeSubsetQuery({ subset, qb, params });
  }
}
```

### 프로필 조회 API

```typescript theme={null}
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
async findById<T extends UserSubsetKey>(
  subset: T,
  id: string
): Promise<UserSubsetMapping[T]> {
  const { rows } = await this.findMany(subset, { id, num: 1, page: 1 });
  if (!rows[0]) {
    throw new NotFoundException("User not found");
  }
  return rows[0];
}

// 사용 예시
const user = await UserModel.findById("P", "123");
// user.employee?.department?.name 접근 가능
```

### 복합 검색 API

```typescript theme={null}
async searchUsers(params: {
  role?: UserRole;
  departmentName?: string;
  minSalary?: number;
}) {
  const { qb, onSubset } = this.getSubsetQueries("P");

  if (params.role) {
    qb.where("users.role", params.role);
  }

  if (params.departmentName) {
    // onSubset으로 Subset P의 테이블에 타입 안전하게 접근
    onSubset("P").where(
      "employee__department.name",
      "like",
      `%${params.departmentName}%`
    );
  }

  if (params.minSalary) {
    onSubset("P").where("employee.salary", ">=", String(params.minSalary));
  }

  qb.orderBy("users.username", "asc");

  return this.executeSubsetQuery({
    subset: "P",
    qb,
    params: { num: 100, page: 1 },
  });
}
```

## Subset 조합

### 여러 Subset 사용 (Union)

```typescript theme={null}
// 상황에 따라 다른 Subset 사용
async getUser(userId: number, detailed: boolean) {
  if (detailed) {
    return await UserModel.findById(userId, ["P"]);
    // 타입: UserSubsetMapping["P"] | undefined
  } else {
    return await UserModel.findById(userId, ["SS"]);
    // 타입: UserSubsetMapping["SS"] | undefined
  }
}
```

### 조건부 Subset 선택

```typescript theme={null}
async getUserData(userId: number, role: UserRole) {
  const subsets = role === "admin" ? ["A"] : ["P"];

  return await UserModel.findById(userId, subsets);
  // 타입: UserSubsetMapping["A"] | UserSubsetMapping["P"] | undefined
}
```

## 성능 최적화

### 필요한 Subset만 사용

```typescript theme={null}
// ❌ 나쁨: 목록에서 Subset A 사용 (과도한 데이터)
const users = await UserModel.findMany({
  listParams: { page: 1, pageSize: 20 },
  subsetKey: "A", // 모든 필드 로딩
});

// ✅ 좋음: 목록에는 Subset L 사용
const users = await UserModel.findMany({
  listParams: { page: 1, pageSize: 20 },
  subsetKey: "L", // 필요한 필드만
});
```

### JOIN 최소화

```typescript theme={null}
// ❌ 나쁨: 불필요한 JOIN (Subset P)
// Subset P는 employee, department 테이블도 JOIN함
const { qb } = UserModel.getSubsetQueries("P");
const result = await UserModel.executeSubsetQuery({
  subset: "P",
  qb,
  params: { num: 100, page: 1 },
});

// ✅ 좋음: JOIN 없는 Subset (SS)
// Subset SS는 users 테이블만 조회
const { qb: qbSS } = UserModel.getSubsetQueries("SS");
const resultSS = await UserModel.executeSubsetQuery({
  subset: "SS",
  qb: qbSS,
  params: { num: 100, page: 1 },
});
```

## Subset과 Raw Puri 비교

### Subset 사용

```typescript theme={null}
// ✅ Subset - 타입 안전, 간결
const { qb, onSubset } = UserModel.getSubsetQueries("P");
qb.where("users.role", "admin");

const result = await UserModel.executeSubsetQuery({
  subset: "P",
  qb,
  params: { num: 20, page: 1 },
});
// 타입: ListResult<..., UserSubsetMapping["P"]>
// JOIN 자동 설정
// SELECT 자동 설정
```

### Raw Puri 사용

```typescript theme={null}
// ⚠️ Raw Puri - 수동 설정
const puri = UserModel.getPuri("r");
const users = await puri
  .table("users")
  .leftJoin("employees", "users.id", "employees.user_id")
  .leftJoin("departments", "employees.department_id", "departments.id")
  .select({
    id: "users.id",
    username: "users.username",
    employee__salary: "employees.salary",
    employee__department__name: "departments.name",
  })
  .where("users.role", "admin");

// 타입: { id: number; username: string; ... }[]
// JOIN 수동 설정
// SELECT 수동 설정
// Hydrate 수동 적용 필요
```

<Tip>
  **Subset을 사용하세요!** - 타입 안전성 보장 - 코드 재사용성 향상 - 유지보수 편리 - 쿼리 일관성
  유지
</Tip>

## 다음 단계

<CardGroup cols={2}>
  <Card title="중첩 관계" icon="diagram-project" href="/ko/database/subsets/nested-relations">
    LoaderQuery로 1:N, N:M 로딩
  </Card>

  <Card title="Subset 정의" icon="pen" href="/ko/database/subsets/defining-subsets">
    Entity에서 Subset 생성
  </Card>

  <Card title="Model 메서드" icon="cube" href="/ko/core-concepts/model/model-methods">
    Model CRUD 메서드
  </Card>

  <Card title="Puri 쿼리" icon="database" href="/ko/database/puri/basic-queries">
    Puri 쿼리 빌더
  </Card>
</CardGroup>
