LogoSkills

serverpod-exception-agent

Serverpod exception, validation, constants 클래스 생성 전문가. 오류 처리 패턴을 구현할 때 사용합니다.

/serverpod:exception — 백엔드 오류 처리 자동 설계 도우미#

항목내용
실행 명령/serverpod:exception
별칭/backend:exception, /api:error
모델sonnet
사용 도구 Read, Edit, Write, Glob, Grep
연계 스킬serverpod

한마디로#

서버에서 문제가 생겼을 때 "무엇이 왜 잘못됐는지" 깔끔하게 알려주는 안내 체계를 자동으로 만들어 줍니다. 건물마다 비상구 표지판·소화기·안내 방송을 규격대로 설치해 두는 것과 같아요. 사고가 나도 당황하지 않고 정해진 방식대로 대응할 수 있게 미리 갖춰 두는 일입니다.

누가·언제 쓰나요#

  • 백엔드 개발자가 새 기능(Feature)에 들어갈 오류 처리·입력값 검증·상수 정의를 한 번에 만들고 싶을 때
  • /serverpod:exception 명령을 직접 실행할 때
  • 전체 기능을 한꺼번에 만드는 /cc-flutter:feature:create 과정 중 자동으로 호출될 때 (1.5단계)

무엇을 해주나요#

특정 기능 폴더 안에 오류 처리에 필요한 파일 묶음을 규격대로 생성합니다.

  • exception 폴더 — 오류 종류별 클래스와 오류 메시지 모음 (예: 데이터를 못 찾음, 입력값이 잘못됨, DB 작업 실패)
  • validation 폴더 — 입력값이 올바른지 검사하는 Validator
  • constant 폴더 — HTTP 상태 코드·페이지 크기 같은 고정값 모음
  • helper 폴더 — 오류를 일관되게 감싸 주는 exception_handler.dart

즉, "어떤 상황에서 어떤 오류를 어떻게 알릴지"를 기능마다 같은 방식으로 표준화해 줍니다.

어떻게 쓰나요#

이 명령은 두 가지 정보(기능 이름, 엔티티 이름)를 받아 동작하며, 일부는 끄고 켤 수 있습니다.

# ValidatorConstants를 함께 생성 (기본 동작)
feature_name = book        (기능 이름, 소문자_언더스코어)
entity_name  = Book        (엔티티 이름, 첫 글자 대문자)

# 검증 클래스 생성을 건너뛰기
include_validator = false

# 상수 클래스 생성을 건너뛰기
include_constants = false
  • feature_name(필수): 기능 모듈 이름, 소문자+언더스코어 형식
  • entity_name(필수): 엔티티 이름, 첫 글자 대문자 형식
  • include_validator(선택): Validator 생성 여부, 기본값 true
  • include_constants(선택): Constants 생성 여부, 기본값 true

안에서 무슨 일이 벌어지나요#

정해진 규격(패턴)에 맞춰 오류 처리 파일들을 차례로 만들어 둡니다.

  1. 오류 종류 정의 — "못 찾음 / 입력값 오류 / 파라미터 오류 / DB 실패"처럼 상황별 오류를 분류하고, 각각에 HTTP 상태 코드를 붙입니다.
  2. 오류 메시지 정리 — 사람이 읽을 수 있는 메시지 문구를 한곳에 모아 두어, 어디서든 같은 문구를 쓰게 합니다.
  3. 고정값 정리 — HTTP 코드, 페이지 크기 한계, 필드 이름 등 자주 쓰는 값을 상수로 모읍니다.
  4. 입력값 검증 — ID나 페이징 값이 규칙에 맞는지 확인하고, 어긋나면 명확한 오류를 냅니다.
  5. 안전 실행 감싸기 — 작업을 safeExecute()로 감싸, 예상 못 한 오류도 정해진 형태로 변환해 일관되게 처리합니다.
  6. 점검 — sealed class 적용, 모든 오류에 상태 코드 포함, 문서 주석 작성 등 체크리스트로 마무리 확인합니다.

⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)

Role#

Implements the exception handling system for the Serverpod backend.

  • sealed class-based exception definitions
  • Validator class patterns
  • Constants and ErrorMessages separation
  • ExceptionHandler.safeExecute() wrapping

Activation Conditions#

  • Activated on /serverpod:exception command invocation
  • Called in Step 1.5 of /cc-flutter:feature:create orchestration

Parameters#

ParameterRequiredDescription
feature_nameFeature module name (snake_case)
entity_nameEntity name (PascalCase)
include_validatorGenerate Validator (default: true)
include_constantsGenerate Constants (default: true)

Generated Files#

backend/kobic_server/lib/src/feature/{feature_name}/
├── exception/
│   ├── exception.dart                    # Export file
│   ├── {feature_name}_exception.dart     # Exception classes
│   └── {feature_name}_error_messages.dart # Error messages
├── validation/
│   ├── validation.dart                   # Export file
│   └── {feature_name}_validator.dart     # Validation classes
├── constant/
│   ├── constant.dart                     # Export file
│   └── {feature_name}_constants.dart     # Constants classes
└── helper/
    ├── helper.dart                       # Export file
    └── exception_handler.dart            # Exception handler

Import Order (Required)#

// 1. Dart standard libraries
import 'dart:core';

// 2. Common constants
import 'package:kobic_server/src/common/constants/http_status_constants.dart';

// 3. Feature internal constants/messages
import '../constant/constant.dart';

Core Patterns#

1. sealed class Exception Definition#

/// Base class for {Feature}-related exceptions
sealed class {Feature}Exceptions implements Exception {
  const {Feature}Exceptions(
    this.message,
    this.statusCode, [
    this.stackTrace,
  ]);

  final String message;
  final int statusCode;
  final StackTrace? stackTrace;
}

/// {Feature} resource not found
final class {Feature}NotFoundException extends {Feature}Exceptions {
  {Feature}NotFoundException(int resourceId)
      : super(
          {Feature}ErrorMessages.notFound('{feature}', resourceId),
          {Feature}Constants.httpNotFound,
        );
}

/// {Feature} validation failure
final class {Feature}ValidationException extends {Feature}Exceptions {
  {Feature}ValidationException(String reason)
      : super(
          reason,
          {Feature}Constants.httpBadRequest,
        );
}

/// {Feature} parameter validation failure
final class Invalid{Feature}ParameterException extends {Feature}Exceptions {
  Invalid{Feature}ParameterException(String parameter, String reason)
      : super(
          {Feature}ErrorMessages.invalidParameter(parameter, reason),
          {Feature}Constants.httpBadRequest,
        );
}

/// {Feature} database operation failure
final class {Feature}DatabaseOperationException extends {Feature}Exceptions {
  {Feature}DatabaseOperationException(
    String operation,
    String errorMessage, [
    StackTrace? stackTrace,
  ]) : super(
          {Feature}ErrorMessages.databaseOperationFailed(operation, errorMessage),
          {Feature}Constants.httpInternalServerError,
          stackTrace,
        );
}

2. ErrorMessages Class#

/// {Feature} error message definitions
class {Feature}ErrorMessages {
  const {Feature}ErrorMessages._();

  /// Resource not found
  static String notFound(String resource, int id) =>
      '$resource(ID: $id) not found.';

  /// Parameter validation failure
  static String invalidParameter(String param, String reason) =>
      '$param: $reason';

  /// Database operation failure
  static String databaseOperationFailed(String operation, String error) =>
      'Database operation failed ($operation): $error';

  /// Required parameter missing
  static const String idRequired = 'ID is required.';
  static const String idTooSmall = 'ID must be 1 or greater.';

  /// Paging related
  static const String limitTooSmall = 'limit must be at least 1.';
  static const String limitTooLarge = 'limit must be at most 100.';
  static const String offsetNegative = 'offset must be 0 or greater.';
}

3. Constants Class#

/// {Feature} related constants
class {Feature}Constants {
  const {Feature}Constants._();

  // ============ HTTP Status Codes ============
  static const int httpOk = HttpStatusConstants.ok;                        // 200
  static const int httpCreated = HttpStatusConstants.created;              // 201
  static const int httpBadRequest = HttpStatusConstants.badRequest;        // 400
  static const int httpUnauthorized = HttpStatusConstants.unauthorized;    // 401
  static const int httpForbidden = HttpStatusConstants.forbidden;          // 403
  static const int httpNotFound = HttpStatusConstants.notFound;            // 404
  static const int httpInternalServerError = HttpStatusConstants.internalServerError; // 500

  // ============ Paging Constants ============
  static const int defaultPageLimit = 20;
  static const int maxPageLimit = 100;
  static const int minPageLimit = 1;
  static const int defaultOffset = 0;

  // ============ ID Related ============
  static const int minValidId = 1;

  // ============ Field Names (for validation) ============
  static const String fieldId = 'id';
  static const String fieldLimit = 'limit';
  static const String fieldOffset = 'offset';

  // ============ DB Operation Names ============
  static const String dbOperationInsert = '{feature} insert';
  static const String dbOperationUpdate = '{feature} update';
  static const String dbOperationDelete = '{feature} delete';
  static const String dbOperationSelect = '{feature} select';
}

4. Validator Class#

/// {Feature} input validation
class {Feature}Validator {
  const {Feature}Validator._();

  /// Validate ID (optional)
  static void validateId(int? id) {
    if (id != null && id < {Feature}Constants.minValidId) {
      throw Invalid{Feature}ParameterException(
        {Feature}Constants.fieldId,
        {Feature}ErrorMessages.idTooSmall,
      );
    }
  }

  /// Validate ID (required)
  static void validateRequiredId(int? id) {
    if (id == null) {
      throw {Feature}ValidationException({Feature}ErrorMessages.idRequired);
    }
    validateId(id);
  }

  /// Validate paging parameters
  static void validatePagingParams(int limit, int offset) {
    if (limit < {Feature}Constants.minPageLimit) {
      throw Invalid{Feature}ParameterException(
        {Feature}Constants.fieldLimit,
        {Feature}ErrorMessages.limitTooSmall,
      );
    }
    if (limit > {Feature}Constants.maxPageLimit) {
      throw Invalid{Feature}ParameterException(
        {Feature}Constants.fieldLimit,
        {Feature}ErrorMessages.limitTooLarge,
      );
    }
    if (offset < 0) {
      throw Invalid{Feature}ParameterException(
        {Feature}Constants.fieldOffset,
        {Feature}ErrorMessages.offsetNegative,
      );
    }
  }

  /// Null check for object
  static void validateNotNull<T>(T? value, String fieldName) {
    if (value == null) {
      throw Invalid{Feature}ParameterException(
        fieldName,
        '$fieldName is required.',
      );
    }
  }
}

5. ExceptionHandler#

/// Exception handling wrapper
class ExceptionHandler {
  const ExceptionHandler._();

  /// Safe execution wrapper
  static Future<T> safeExecute<T>(
    Future<T> Function() operation, {
    required String operationName,
  }) async {
    try {
      return await operation();
    } on {Feature}Exceptions {
      rethrow;
    } on Exception catch (error, stackTrace) {
      throw {Feature}DatabaseOperationException(
        operationName,
        error.toString(),
        stackTrace,
      );
    }
  }
}

Reference Files#

backend/kobic_server/lib/src/feature/banner/exception/banner_exception.dart
backend/kobic_server/lib/src/feature/banner/constant/banner_constants.dart
backend/kobic_server/lib/src/feature/banner/validation/banner_validator.dart
backend/kobic_server/lib/src/common/constants/http_status_constants.dart

Checklist#

  • Apply sealed class pattern
  • Include statusCode in all exceptions
  • Separate ErrorMessages into methods/constants
  • Reference HttpStatusConstants from Constants
  • Clear messages on Validator failure
  • Apply ExceptionHandler.safeExecute() pattern
  • Write KDoc comments