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

# 에러 처리

> SoException 기반의 일관된 에러 응답

API에서 발생하는 에러를 일관되게 처리하고 명확한 응답을 제공하는 방법을 알아봅니다.

## 에러 처리 개요

<CardGroup cols={2}>
  <Card title="일관된 응답" icon="equals">
    표준화된 에러 형식 클라이언트 친화적
  </Card>

  <Card title="상태 코드" icon="hashtag">
    HTTP 상태 코드 RESTful 표준
  </Card>

  <Card title="명확한 메시지" icon="message">
    개발자 친화적 사용자 친화적
  </Card>

  <Card title="로깅" icon="file-lines">
    에러 추적 디버깅 지원
  </Card>
</CardGroup>

## SoException

Sonamu는 `SoException` 기반의 에러 클래스를 제공합니다. `SoException`을 throw하면 프레임워크가 자동으로 적절한 HTTP 상태 코드와 구조화된 에러 응답을 전송합니다.

### 기본 사용법

```typescript theme={null}
import { NotFoundException } from "sonamu";

class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async get(id: number): Promise<User> {
    const rdb = this.getPuri("r");

    const user = await rdb.table("users").where("id", id).first();

    if (!user) {
      throw new NotFoundException("사용자를 찾을 수 없습니다");
    }

    return user;
  }
}
```

### 제공되는 예외 클래스

Sonamu가 기본으로 제공하는 예외 클래스입니다. 모두 `SoException`을 상속합니다.

| 클래스                            | 상태 코드 | 사용 시기                    |
| ------------------------------ | ----- | ------------------------ |
| `BadRequestException`          | 400   | 잘못된 매개변수 등 요청에 문제가 있는 경우 |
| `UnauthorizedException`        | 401   | 로그인이 필요하거나 접근 권한이 없는 경우  |
| `NotFoundException`            | 404   | 존재하지 않는 레코드에 접근하는 경우     |
| `InternalServerErrorException` | 500   | 내부 처리 로직 또는 외부 API 호출 오류 |
| `ServiceUnavailableException`  | 503   | 현재 상태에서 처리가 불가능한 경우      |
| `TargetNotFoundException`      | 520   | 처리 대상이 존재하지 않는 경우        |
| `AlreadyProcessedException`    | 541   | 이미 처리된 요청인 경우            |
| `DuplicateRowException`        | 542   | 중복을 허용하지 않는 케이스에 중복 요청   |

### SoException 생성자

모든 예외 클래스는 동일한 생성자 시그니처를 가집니다.

```typescript theme={null}
constructor(message: LocalizedString, payload?: unknown)
```

* `message`: 에러 메시지. 다국어 문자열(`LocalizedString`)을 지원합니다.
* `payload`: 추가 정보. Zod 검증 이슈 배열 등을 전달할 수 있습니다.

## 사용 예제

### 다양한 에러 상황

```typescript theme={null}
import {
  BadRequestException,
  UnauthorizedException,
  NotFoundException,
  DuplicateRowException,
} from "sonamu";

class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async get(id: number): Promise<User> {
    const rdb = this.getPuri("r");

    const user = await rdb.table("users").where("id", id).first();

    if (!user) {
      throw new NotFoundException("사용자를 찾을 수 없습니다");
    }

    return user;
  }

  @api({ httpMethod: "POST" })
  async create(params: CreateUserParams): Promise<{ userId: number }> {
    const context = Sonamu.getContext();

    // 인증 확인
    if (!context.user) {
      throw new UnauthorizedException("로그인이 필요합니다");
    }

    // 입력 검증
    if (!params.email) {
      throw new BadRequestException("이메일은 필수입니다");
    }

    const rdb = this.getPuri("r");

    // 중복 확인
    const existing = await rdb.table("users").where("email", params.email).first();

    if (existing) {
      throw new DuplicateRowException("이미 사용 중인 이메일입니다");
    }

    const wdb = this.getPuri("w");
    const [user] = await wdb.table("users").insert(params).returning("id");

    return { userId: user.id };
  }
}
```

### payload를 활용한 상세 정보 전달

```typescript theme={null}
import { BadRequestException } from "sonamu";

class OrderModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: CreateOrderParams): Promise<{ orderId: number }> {
    // payload로 에러 상세 정보 전달
    if (params.quantity <= 0) {
      throw new BadRequestException("주문 수량이 올바르지 않습니다", {
        field: "quantity",
        value: params.quantity,
        constraint: "must be > 0",
      });
    }

    // ...
  }
}
```

### isSoException 타입 가드

```typescript theme={null}
import { isSoException, AlreadyProcessedException } from "sonamu";

class PaymentModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async process(paymentId: number): Promise<void> {
    try {
      await this.executePayment(paymentId);
    } catch (error) {
      if (isSoException(error) && error.statusCode === 541) {
        // 이미 처리된 결제 - 무시
        return;
      }
      throw error;
    }
  }
}
```

## Zod 검증 에러 처리

`BadRequestException`의 `payload`에 Zod 이슈 배열을 전달하면, Sonamu의 에러 핸들러가 자동으로 검증 에러 상세 정보를 응답에 포함합니다.

### Zod 에러 변환

```typescript theme={null}
import { z } from "zod";
import { BadRequestException } from "sonamu";

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2),
  password: z.string().min(8),
});

class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: unknown): Promise<{ userId: number }> {
    const result = CreateUserSchema.safeParse(params);

    if (!result.success) {
      throw new BadRequestException("검증 실패", result.error.issues);
    }

    const validated = result.data;

    const wdb = this.getPuri("w");
    const [user] = await wdb.table("users").insert(validated).returning("id");

    return { userId: user.id };
  }
}
```

## 에러 응답 형식

Sonamu의 내장 에러 핸들러는 다음 형식으로 응답합니다.

### 기본 에러 응답

```json theme={null}
{
  "name": "NotFoundException",
  "code": null,
  "message": "사용자를 찾을 수 없습니다"
}
```

### Zod 검증 에러 응답 (payload에 이슈 배열 전달 시)

```json theme={null}
{
  "name": "BadRequestException",
  "code": null,
  "message": "검증 실패 (email)",
  "issues": [
    {
      "code": "invalid_type",
      "expected": "string",
      "received": "undefined",
      "path": ["email"],
      "message": "Required"
    }
  ]
}
```

## 에러 핸들러 커스터마이징

Sonamu는 기본 에러 핸들러를 내장하고 있지만, 필요 시 `startServer`의 `lifecycle.onError` 옵션으로 커스텀 에러 핸들러를 설정할 수 있습니다.

```typescript theme={null}
import { Sonamu, isSoException } from "sonamu";

Sonamu.startServer({
  lifecycle: {
    onError: (error, request, reply) => {
      // 커스텀 에러 처리 로직
      if (isSoException(error)) {
        reply.status(error.statusCode).send({
          error: error.message,
          payload: error.payload,
        });
      } else {
        reply.status(500).send({
          error: "Internal server error",
        });
      }
    },
  },
});
```

## 상태 코드 참조표

### 표준 HTTP 상태 코드

| 코드  | 이름                    | 사용 시기              |
| --- | --------------------- | ------------------ |
| 200 | OK                    | 성공 (GET, PUT)      |
| 201 | Created               | 리소스 생성 성공 (POST)   |
| 204 | No Content            | 성공, 응답 없음 (DELETE) |
| 400 | Bad Request           | 잘못된 요청 형식          |
| 401 | Unauthorized          | 인증 필요              |
| 404 | Not Found             | 리소스 없음             |
| 500 | Internal Server Error | 서버 내부 에러           |
| 503 | Service Unavailable   | 서비스 이용 불가          |

### Sonamu 커스텀 상태 코드

| 코드  | 예외 클래스                      | 사용 시기            |
| --- | --------------------------- | ---------------- |
| 520 | `TargetNotFoundException`   | 처리 대상이 존재하지 않음   |
| 541 | `AlreadyProcessedException` | 이미 처리된 요청        |
| 542 | `DuplicateRowException`     | 중복 허용하지 않는 곳에 중복 |

## 실전 패턴

```typescript theme={null}
import {
  UnauthorizedException,
  BadRequestException,
  DuplicateRowException,
} from "sonamu";

class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: unknown): Promise<{ userId: number }> {
    const context = Sonamu.getContext();

    // 1. 인증 확인
    if (!context.user) {
      throw new UnauthorizedException("로그인이 필요합니다");
    }

    // 2. Zod 검증
    const validated = CreateUserSchema.safeParse(params);
    if (!validated.success) {
      throw new BadRequestException("검증 실패", validated.error.issues);
    }

    const data = validated.data;

    // 3. 비즈니스 규칙 검증
    const rdb = this.getPuri("r");
    const existing = await rdb.table("users").where("email", data.email).first();

    if (existing) {
      throw new DuplicateRowException("이미 사용 중인 이메일입니다");
    }

    // 4. 생성
    const wdb = this.getPuri("w");
    const [user] = await wdb.table("users").insert(data).returning("id");

    return { userId: user.id };
  }
}
```

## 주의사항

<Warning>
  **에러 처리 시 주의사항**: 1. 민감한 정보 노출 금지 (스택 트레이스, DB 에러 등) 2. 적절한 HTTP
  상태 코드 사용 3. 명확하고 일관된 에러 메시지 4. 에러는 항상 로깅 5. 프로덕션에서는 상세 정보 제한
</Warning>

## 다음 단계

<CardGroup cols={2}>
  <Card title="자동 검증" icon="wand-magic-sparkles" href="/ko/api-development/validation/automatic-validation">
    Zod 기반 검증
  </Card>

  <Card title="커스텀 검증" icon="code" href="/ko/api-development/validation/custom-validation">
    커스텀 검증 로직
  </Card>

  <Card title="@api 데코레이터" icon="at" href="/ko/api-development/creating-apis/api-decorator">
    API 기본 사용법
  </Card>

  <Card title="Context" icon="cube" href="/ko/api-development/context">
    SonamuContext
  </Card>
</CardGroup>
