메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°
SoException은 Sonamu의 λͺ¨λ“  μ˜ˆμ™Έ ν΄λž˜μŠ€κ°€ μƒμ†ν•˜λŠ” 좔상 κΈ°λ³Έ ν΄λž˜μŠ€μž…λ‹ˆλ‹€. HTTP μƒνƒœ μ½”λ“œμ™€ λ©”μ‹œμ§€, μΆ”κ°€ νŽ˜μ΄λ‘œλ“œλ₯Ό 포함할 수 μžˆμŠ΅λ‹ˆλ‹€.

νƒ€μž… μ •μ˜

abstract class SoException extends Error {
  constructor(
    public readonly statusCode: number,
    public message: LocalizedString,
    public payload?: unknown,
  );
}

속성

statusCode

readonly statusCode: number
HTTP 응닡 μƒνƒœ μ½”λ“œμž…λ‹ˆλ‹€. μ˜ˆμ™Έκ°€ λ°œμƒν•˜λ©΄ Fastifyκ°€ 이 μ½”λ“œλ‘œ μ‘λ‹΅ν•©λ‹ˆλ‹€.

message

message: LocalizedString;
μ˜ˆμ™Έ λ©”μ‹œμ§€μž…λ‹ˆλ‹€. LocalizedString νƒ€μž…μœΌλ‘œ, Sonamu의 λ‹€κ΅­μ–΄ 지원 μ‹œμŠ€ν…œμ„ 톡해 μ§€μ—­ν™”λœ λ¬Έμžμ—΄μ„ μ „λ‹¬ν•©λ‹ˆλ‹€.

payload

payload?: unknown
선택적 μΆ”κ°€ λ°μ΄ν„°μž…λ‹ˆλ‹€. μ˜ˆμ™Έμ™€ κ΄€λ ¨λœ 상세 정보λ₯Ό 전달할 λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€. μ‚¬μš© μ˜ˆμ‹œ:
throw new BadRequestException("μœ νš¨ν•˜μ§€ μ•Šμ€ 이메일 ν˜•μ‹μž…λ‹ˆλ‹€", {
  field: "email",
  providedValue: "invalid-email",
  expectedFormat: "user@example.com",
});

μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜

isSoException()

μ£Όμ–΄μ§„ 값이 SoException μΈμŠ€ν„΄μŠ€μΈμ§€ ν™•μΈν•©λ‹ˆλ‹€.
function isSoException(err: unknown): err is SoException;
μ‚¬μš© μ˜ˆμ‹œ:
try {
  // μ–΄λ–€ μž‘μ—…
} catch (err) {
  if (isSoException(err)) {
    console.log(`Status: ${err.statusCode}`);
    console.log(`Message: ${err.message}`);
    console.log(`Payload:`, err.payload);
  } else {
    // λ‹€λ₯Έ νƒ€μž…μ˜ μ—λŸ¬ 처리
  }
}

λ‚΄μž₯ μ˜ˆμ™Έ 클래슀

SonamuλŠ” 일반적인 HTTP 였λ₯˜ 상황을 μœ„ν•œ μ—¬λŸ¬ λ‚΄μž₯ μ˜ˆμ™Έ 클래슀λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.

BadRequestException (400)

잘λͺ»λœ λ§€κ°œλ³€μˆ˜ λ“± μš”μ²­μ‚¬ν•­μ— λ¬Έμ œκ°€ μžˆλŠ” 경우 μ‚¬μš©ν•©λ‹ˆλ‹€.
class BadRequestException extends SoException
μ‚¬μš© μ˜ˆμ‹œ:
@api()
async createUser(email: string, password: string) {
  if (!email.includes("@")) {
    throw new BadRequestException("μœ νš¨ν•˜μ§€ μ•Šμ€ 이메일 ν˜•μ‹μž…λ‹ˆλ‹€");
  }

  if (password.length < 8) {
    throw new BadRequestException("λΉ„λ°€λ²ˆν˜ΈλŠ” μ΅œμ†Œ 8자 이상이어야 ν•©λ‹ˆλ‹€", {
      minLength: 8,
      providedLength: password.length
    });
  }

  // μ‚¬μš©μž 생성 둜직
}

UnauthorizedException (401)

둜그인이 ν•„μš”ν•œ 경우 λ‘œκ·Έμ•„μ›ƒ μƒνƒœμ΄κ±°λ‚˜ μ ‘κ·Ό κΆŒν•œμ΄ μ—†λŠ” μš”μ²­ μ‹œ μ‚¬μš©ν•©λ‹ˆλ‹€.
class UnauthorizedException extends SoException
μ‚¬μš© μ˜ˆμ‹œ:
@api()
async getMyProfile(ctx: Context) {
  if (!ctx.user) {
    throw new UnauthorizedException("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€");
  }

  return this.findById(ctx.user.id);
}

NotFoundException (404)

μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” λ ˆμ½”λ“œμ— μ ‘κ·Ό μ‹œ μ‚¬μš©ν•©λ‹ˆλ‹€.
class NotFoundException extends SoException
μ‚¬μš© μ˜ˆμ‹œ:
@api()
async getUserById(id: number) {
  const user = await this.findById(id);

  if (!user) {
    throw new NotFoundException(`μ‚¬μš©μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€ (ID: ${id})`);
  }

  return user;
}

InternalServerErrorException (500)

λ‚΄λΆ€ 처리 둜직(μ™ΈλΆ€ API 호좜 포함) 였λ₯˜ λ°œμƒ μ‹œ μ‚¬μš©ν•©λ‹ˆλ‹€.
class InternalServerErrorException extends SoException
μ‚¬μš© μ˜ˆμ‹œ:
@api()
async processPayment(orderId: number) {
  try {
    const result = await externalPaymentAPI.charge(orderId);
    return result;
  } catch (err) {
    throw new InternalServerErrorException(
      "결제 처리 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€",
      { orderId, originalError: err }
    );
  }
}

ServiceUnavailableException (503)

ν˜„μž¬ μƒνƒœμ—μ„œ μ²˜λ¦¬κ°€ λΆˆκ°€λŠ₯ν•œ 경우 μ‚¬μš©ν•©λ‹ˆλ‹€.
class ServiceUnavailableException extends SoException
μ‚¬μš© μ˜ˆμ‹œ:
@api()
async getRecommendations() {
  if (!this.mlServiceAvailable) {
    throw new ServiceUnavailableException(
      "μΆ”μ²œ μ„œλΉ„μŠ€κ°€ μΌμ‹œμ μœΌλ‘œ μ‚¬μš© λΆˆκ°€λŠ₯ν•©λ‹ˆλ‹€"
    );
  }

  return this.fetchRecommendations();
}

TargetNotFoundException (520)

μž‘μ—… λŒ€μƒμ΄ μ—†λŠ” 경우 μ‚¬μš©ν•©λ‹ˆλ‹€.
class TargetNotFoundException extends SoException
μ‚¬μš© μ˜ˆμ‹œ:
@api()
async deleteUserPosts(userId: number) {
  const posts = await this.findByUserId(userId);

  if (posts.length === 0) {
    throw new TargetNotFoundException(
      `μ‚­μ œν•  κ²Œμ‹œλ¬Όμ΄ μ—†μŠ΅λ‹ˆλ‹€ (μ‚¬μš©μž ID: ${userId})`
    );
  }

  await this.deleteMany(posts.map(p => p.id));
}

AlreadyProcessedException (541)

이미 처리된 μš”μ²­μ— λŒ€ν•œ 쀑볡 처리 μ‹œλ„ μ‹œ μ‚¬μš©ν•©λ‹ˆλ‹€.
class AlreadyProcessedException extends SoException
μ‚¬μš© μ˜ˆμ‹œ:
@api()
async processOrder(orderId: number) {
  const order = await this.findById(orderId);

  if (order.status === "processed") {
    throw new AlreadyProcessedException(
      `주문이 이미 μ²˜λ¦¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€ (ID: ${orderId})`,
      { processedAt: order.processedAt }
    );
  }

  // 주문 처리 둜직
}

DuplicateRowException (542)

쀑볡을 ν—ˆμš©ν•˜μ§€ μ•ŠλŠ” 경우 쀑볡 μš”μ²­ μ‹œ μ‚¬μš©ν•©λ‹ˆλ‹€.
class DuplicateRowException extends SoException
μ‚¬μš© μ˜ˆμ‹œ:
@api()
async createUser(email: string, username: string) {
  const existingUser = await this.findByEmail(email);

  if (existingUser) {
    throw new DuplicateRowException(
      "이미 λ“±λ‘λœ μ΄λ©”μΌμž…λ‹ˆλ‹€",
      { field: "email", value: email }
    );
  }

  // μ‚¬μš©μž 생성 둜직
}

μ˜ˆμ™Έ 처리 흐름

SonamuλŠ” λ°œμƒν•œ μ˜ˆμ™Έλ₯Ό μžλ™μœΌλ‘œ μ²˜λ¦¬ν•˜μ—¬ μ μ ˆν•œ HTTP μ‘λ‹΅μœΌλ‘œ λ³€ν™˜ν•©λ‹ˆλ‹€:
// API λ©”μ„œλ“œμ—μ„œ μ˜ˆμ™Έ λ°œμƒ
@api()
async createPost(title: string) {
  if (!title) {
    throw new BadRequestException("제λͺ©μ€ ν•„μˆ˜μž…λ‹ˆλ‹€");
  }
  // ...
}

// ν΄λΌμ΄μ–ΈνŠΈλŠ” λ‹€μŒκ³Ό 같은 응닡을 λ°›μŒ:
// HTTP 400 Bad Request
// {
//   "statusCode": 400,
//   "message": "제λͺ©μ€ ν•„μˆ˜μž…λ‹ˆλ‹€"
// }
payloadκ°€ μžˆλŠ” 경우:
throw new BadRequestException("μœ νš¨ν•˜μ§€ μ•Šμ€ 데이터", {
  errors: [
    { field: "email", message: "이메일 ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€" },
    { field: "age", message: "λ‚˜μ΄λŠ” 0보닀 컀야 ν•©λ‹ˆλ‹€" },
  ],
});

// ν΄λΌμ΄μ–ΈνŠΈ 응닡:
// HTTP 400 Bad Request
// {
//   "statusCode": 400,
//   "message": "μœ νš¨ν•˜μ§€ μ•Šμ€ 데이터",
//   "payload": {
//     "errors": [
//       { "field": "email", "message": "이메일 ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€" },
//       { "field": "age", "message": "λ‚˜μ΄λŠ” 0보닀 컀야 ν•©λ‹ˆλ‹€" }
//     ]
//   }
// }

μ˜ˆμ™Έ vs Guards

κ°„λ‹¨ν•œ 인증 체크의 경우 Guardsλ₯Ό μ‚¬μš©ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€:
// Guards μ‚¬μš© (ꢌμž₯)
@api({ guards: ["user"] })
async getMyData(ctx: Context) {
  // ctx.userκ°€ 보μž₯됨
  return this.findById(ctx.user!.id);
}

// μˆ˜λ™ 체크 (더 λ³΅μž‘ν•œ λ‘œμ§μ— μ‚¬μš©)
@api()
async updateProfile(ctx: Context, data: ProfileData) {
  if (!ctx.user) {
    throw new UnauthorizedException("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€");
  }

  if (ctx.user.id !== data.userId) {
    throw new UnauthorizedException("본인의 ν”„λ‘œν•„λ§Œ μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€");
  }

  // ν”„λ‘œν•„ μ—…λ°μ΄νŠΈ 둜직
}

κ΄€λ ¨ λ¬Έμ„œ