> ## 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.

# findById

> ID로 단일 레코드 조회

`findById`는 ID로 단일 레코드를 조회하는 메서드입니다. 지정된 서브셋으로 데이터를 로드하며, 레코드가 없으면 `NotFoundException`을 발생시킵니다.

<Info>
  `findById`는 BaseModelClass에 정의되어 있지 않습니다. Entity를 생성하면 Syncer가 각 Model 클래스에
  자동으로 생성하는 **표준 패턴**입니다.
</Info>

## 타입 시그니처

```typescript theme={null}
async findById<T extends SubsetKey>(
  subset: T,
  id: number
): Promise<SubsetMapping[T]>
```

## 자동 생성 코드

Sonamu는 Entity를 기반으로 다음 코드를 자동으로 생성합니다:

```typescript theme={null}
// src/application/user/user.model.ts (자동 생성)
class UserModelClass extends BaseModelClass {
  @api({ httpMethod: "GET", clients: ["axios", "tanstack-query"], resourceName: "User" })
  async findById<T extends UserSubsetKey>(subset: T, id: number): Promise<UserSubsetMapping[T]> {
    const { rows } = await this.findMany(subset, {
      id,
      num: 1,
      page: 1,
    });
    if (!rows[0]) {
      throw new NotFoundException(`존재하지 않는 User ID ${id}`);
    }

    return rows[0];
  }
}
```

**내부 동작:**

1. `findMany`를 호출하여 ID로 조회
2. `num: 1, page: 1`로 단건만 가져옴
3. 결과가 없으면 `NotFoundException` 발생
4. 첫 번째 레코드 반환

## 매개변수

### subset

조회할 데이터의 서브셋을 지정합니다.

**타입:** `SubsetKey` (예: `"A"`, `"B"`, `"C"`)

서브셋은 Entity 정의에서 설정한 데이터 형태를 결정합니다. 각 서브셋은 다른 필드와 관계를 포함할 수 있습니다.

```typescript theme={null}
// 기본 정보만 조회
const user = await UserModel.findById("A", 1);

// 관계 데이터 포함 조회
const user = await UserModel.findById("B", 1);
```

### id

조회할 레코드의 ID입니다.

**타입:** `number`

```typescript theme={null}
const user = await UserModel.findById("A", 123);
```

## 반환값

**타입:** `Promise<SubsetMapping[T]>`

지정된 서브셋 타입의 레코드를 반환합니다. 타입은 서브셋에 따라 자동으로 추론됩니다.

```typescript theme={null}
// user의 타입은 자동으로 UserSubsetMapping["A"]로 추론됨
const user = await UserModel.findById("A", 1);

console.log(user.id); // number
console.log(user.email); // string
console.log(user.name); // string
```

## 예외

### NotFoundException

ID에 해당하는 레코드가 없으면 `NotFoundException`이 발생합니다.

```typescript theme={null}
try {
  const user = await UserModel.findById("A", 999);
} catch (error) {
  if (error instanceof NotFoundException) {
    console.error("사용자를 찾을 수 없습니다");
  }
}
```

**HTTP 상태 코드:** 404

## 기본 사용법

### 단순 조회

```typescript theme={null}
import { UserModel } from "./user/user.model";

class UserService {
  async getUser(userId: number) {
    // ID로 사용자 조회 (서브셋 A)
    const user = await UserModel.findById("A", userId);

    return {
      id: user.id,
      email: user.email,
      name: user.name,
    };
  }
}
```

### API에서 사용

`findById`는 자동으로 `@api` 데코레이터가 적용되어 REST API로 노출됩니다:

```typescript theme={null}
// 자동 생성된 코드
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"], resourceName: "User" })
async findById<T extends UserSubsetKey>(
  subset: T,
  id: number
): Promise<UserSubsetMapping[T]> {
  // ...
}
```

생성된 클라이언트 코드:

```typescript theme={null}
// services.generated.ts (자동 생성)
import { UserService } from "@/services/UserService";

// ID로 사용자 조회
const user = await UserService.getUser("A", 123);
```

## 서브셋별 사용

### 서브셋 A (기본)

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

// 사용 가능한 필드
user.id; // number
user.email; // string
user.name; // string
user.created_at; // Date
```

### 서브셋 B (관계 포함)

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

// 관계 데이터 자동 로드
user.id; // number
user.email; // string
user.name; // string
user.posts; // Post[] (1:N 관계)
user.profile; // Profile (1:1 관계)
```

### 서브셋 C (중첩 관계)

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

// 중첩 관계까지 로드
user.posts[0].comments; // Comment[] (중첩 관계)
user.profile.images; // Image[] (중첩 관계)
```

## 실전 예시

<Tabs>
  <Tab title="사용자 프로필" icon="user">
    ```typescript theme={null}
    import { UserModel } from "./user/user.model";
    import { api } from "sonamu";

    class ProfileFrame {
      @api({ httpMethod: "GET" })
      async getProfile(userId: number) {
        // ID로 사용자 조회 (프로필 정보 포함)
        const user = await UserModel.findById("Profile", userId);

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          avatar: user.profile?.avatar_url,
          bio: user.profile?.bio,
          posts_count: user.posts?.length ?? 0
        };
      }
    }
    ```
  </Tab>

  <Tab title="주문 상세" icon="cart-shopping">
    ```typescript theme={null}
    import { OrderModel } from "./order/order.model";
    import { api } from "sonamu";

    class OrderFrame {
      @api({ httpMethod: "GET" })
      async getOrderDetail(orderId: number) {
        // 주문 상세 조회 (상품, 배송 정보 포함)
        const order = await OrderModel.findById("Detail", orderId);

        return {
          id: order.id,
          status: order.status,
          total_amount: order.total_amount,
          items: order.items.map(item => ({
            product_name: item.product.name,
            quantity: item.quantity,
            price: item.price
          })),
          shipping: {
            address: order.shipping_address,
            status: order.shipping_status,
            tracking_number: order.tracking_number
          },
          customer: {
            name: order.user.name,
            email: order.user.email
          }
        };
      }
    }
    ```
  </Tab>

  <Tab title="권한 확인" icon="shield">
    ```typescript theme={null}
    import { UserModel } from "./user/user.model";
    import { Sonamu, api, UnauthorizedException } from "sonamu";

    class PostFrame {
      @api({ httpMethod: "POST" })
      async deletePost(postId: number) {
        const { user } = Sonamu.getContext();

        // 게시물 작성자 확인
        const post = await PostModel.findById("A", postId);

        if (post.user_id !== user.id) {
          throw new UnauthorizedException("본인의 게시물만 삭제할 수 있습니다");
        }

        await PostModel.del([postId]);

        return { success: true };
      }
    }
    ```
  </Tab>

  <Tab title="에러 처리" icon="circle-exclamation">
    ```typescript theme={null}
    import { UserModel } from "./user/user.model";
    import { NotFoundException, api } from "sonamu";

    class UserFrame {
      @api({ httpMethod: "GET" })
      async getUserSafely(userId: number) {
        try {
          // ID로 사용자 조회
          const user = await UserModel.findById("A", userId);

          return {
            exists: true,
            user: {
              id: user.id,
              name: user.name,
              email: user.email
            }
          };
        } catch (error) {
          if (error instanceof NotFoundException) {
            return {
              exists: false,
              user: null
            };
          }
          throw error;
        }
      }

      @api({ httpMethod: "GET" })
      async getMultipleUsers(userIds: number[]) {
        const users = [];

        for (const id of userIds) {
          try {
            const user = await UserModel.findById("A", id);
            users.push(user);
          } catch (error) {
            // 존재하지 않는 사용자는 건너뜀
            if (error instanceof NotFoundException) {
              continue;
            }
            throw error;
          }
        }

        return users;
      }
    }
    ```
  </Tab>
</Tabs>

## findById vs findOne vs findMany

### findById

* ID로 **단일** 레코드 조회
* 레코드가 없으면 **예외 발생**
* 가장 간단하고 빠름
* **자동 생성됨**

```typescript theme={null}
// ID로 조회 (없으면 예외)
const user = await UserModel.findById("A", 1);
```

### findOne

* **조건**으로 단일 레코드 조회
* 레코드가 없으면 **null 반환**
* 복잡한 조건 가능
* **직접 구현 필요** (선택적)

```typescript theme={null}
// 조건으로 조회 (없으면 null)
const user = await UserModel.findOne("A", {
  email: "user@example.com",
});
```

### findMany

* **조건**으로 여러 레코드 조회
* **페이지네이션** 지원
* **총 개수** 반환
* **자동 생성됨**

```typescript theme={null}
// 리스트 조회 (페이지네이션)
const { rows, total } = await UserModel.findMany("A", {
  status: "active",
  num: 20,
  page: 1,
});
```

## 타입 안정성

서브셋에 따라 반환 타입이 자동으로 추론됩니다:

```typescript theme={null}
// 서브셋 A: 기본 필드만
const userA = await UserModel.findById("A", 1);
userA.posts; // ❌ 타입 에러: Property 'posts' does not exist

// 서브셋 B: posts 포함
const userB = await UserModel.findById("B", 1);
userB.posts; // ✅ Post[]
```

## 클라이언트 사용

### React (TanStack Query)

```typescript theme={null}
import { useQuery } from "@tanstack/react-query";
import { UserService } from "@/services/UserService";

function UserProfile({ userId }: { userId: number }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ["user", userId],
    queryFn: () => UserService.getUser("A", userId)
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>User not found</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}
```

### Vue

```typescript theme={null}
import { UserService } from "@/services/UserService";

export default {
  data() {
    return {
      user: null,
      loading: false,
      error: null,
    };
  },
  async mounted() {
    this.loading = true;
    try {
      this.user = await UserService.getUser("A", this.userId);
    } catch (error) {
      this.error = error.message;
    } finally {
      this.loading = false;
    }
  },
};
```

## 주의사항

### 1. 서브셋 선택

필요한 데이터만 포함하는 서브셋을 선택하세요. 불필요한 관계를 로드하면 성능이 저하됩니다.

```typescript theme={null}
// ❌ 나쁜 예: 모든 관계 로드
const user = await UserModel.findById("Full", 1);

// ✅ 좋은 예: 필요한 필드만
const user = await UserModel.findById("A", 1);
```

### 2. 예외 처리

`findById`는 레코드가 없으면 예외를 발생시킵니다. 필요한 경우 예외 처리를 추가하세요.

```typescript theme={null}
// ❌ 나쁜 예: 예외 처리 없음
const user = await UserModel.findById("A", userId); // 404 에러 가능

// ✅ 좋은 예: 예외 처리
try {
  const user = await UserModel.findById("A", userId);
} catch (error) {
  if (error instanceof NotFoundException) {
    // 적절한 처리
  }
}
```

### 3. null 가능성

레코드 존재 여부가 불확실하면 `findOne`을 사용하세요 (직접 구현 필요).

```typescript theme={null}
// findById: 레코드가 반드시 존재해야 함
const user = await UserModel.findById("A", 1);

// findOne: 레코드가 없을 수 있음 (직접 구현 필요)
const user = await UserModel.findOne("A", { email: "unknown@example.com" });
if (user === null) {
  // 레코드 없음 처리
}
```

## 다음 단계

<CardGroup cols={2}>
  <Card title="findMany" icon="list" href="/ko/api-reference/base-model/find-many">
    리스트 조회 및 페이지네이션
  </Card>

  <Card title="save" icon="floppy-disk" href="/ko/api-reference/base-model/save">
    레코드 저장 및 수정
  </Card>

  <Card title="Subset" icon="layer-group" href="/ko/database/subsets/what-are-subsets">
    서브셋 이해하기
  </Card>

  <Card title="예외 처리" icon="triangle-exclamation" href="/ko/api-reference/exceptions/not-found-error">
    NotFoundException 처리
  </Card>
</CardGroup>
