Files
Gnuboard7/docs/backend/service-provider.md
T
HeuJung ffab451b4a fix(core,installer): dev vendor 설치본의 코어 업데이트 중단 수정 — stale 패키지 매니페스트 3계층
Laravel PackageManifest 는 bootstrap/cache/packages.php 가 있으면 stale 여부를 검사하지
않고 그대로 읽어 등재된 provider 를 new 한다. 코어 업데이트는 Step 6/8 에서 vendor 를
--no-dev 로 교체하지만 그 파일은 Step 11 까지 이전 설치본의 것이 남으므로, 옵션 없는
`composer install` 로 깔린 사이트에서는 Step 10 spawn 자식이 새 vendor 에 없는 provider 를
찾다 부팅 단계에서 죽는다. 부팅 전이라 앱 로그에 흔적이 없고 부모에게는 자식의 비정상
종료로만 보여, 운영자에게는 「Class ... not found」 와 수동 재개 안내만 남는다.
(sir.kr 커뮤니티 제보, 7.0.9 → 7.0.10)

3계층으로 막는다.

 1. 부모 — spawn 직전 PackageManifestCacheHelper::clear
 2. 자식 — bootstrap/app.php 가 G7_UPDATE_IN_PROGRESS 를 보고 스스로 정리한다.
 이미 배포된 7.0.9·7.0.10 부모는 고칠 수 없으므로 그 아래에서 도는 신버전
 자식의 유일한 방어다. App\ 클래스를 참조하지 않고 실패는 무시한다.
 3. 범위 — CoreVersionChecker 의 env APP_VERSION 우선을 CoreUpdateContext 트리 안으로
 축소한다. 업데이트 전에 뜬 artisan serve·큐 워커가 옛 값을 물고 확장을
 incompatible_core 로 끄던 경로를 닫는다(관리자 템플릿이 대상이면 복구 UI
 자체에 도달할 수 없다).

업데이트 트리 판정은 App\Support\CoreUpdateContext 가 단독 소유하고
CoreServiceProvider::isCoreUpdateInProgress 는 위임으로 남는다. bootstrap/app.php 의
복제본은 부팅 전이라 불가피한 예외이며, 두 조건의 동형성을 테스트가 단언한다.

실측 중 드러난 결함 2건을 함께 고쳤다.

 - ConfigCacheHelper::withPreservedContainer 가 파사드 애플리케이션을 되돌리지 않아
 Step 11 이 `Target class [command.tinker] does not exist` 로 실패·롤백했다.
 - updateVersionInEnv 가 프로세스 환경을 갱신하지 않아 config 캐시에 이전 버전이 구워졌다
 (Laravel env 저장소가 불변이라 재부팅으로도 덮이지 않는다).

인스톨러는 재사용 vendor 의 개발용 패키지를 installed.json 으로 감지해 설치 환경 확인
카드·설치 로그로 알리되 설치를 차단하지 않고( 결정 D1), 재사용 경로에서도 컴파일 캐시를
정리한다. 실행되는 명령만이 아니라 실패 시 안내하는 수동 명령까지 --no-dev 로 맞췄다.
코어 업데이트 완료·핸드오프·단독 재개 사후 단계에서 queue:restart 신호를 보낸다(D3).

코어 7.0.10 → 7.0.11.
2026-09-08 09:46:57 +09:00

22 KiB

서비스 프로바이더 안전성

목차: index.md | enum.md | controllers.md | service-repository.md | validation.md | exceptions.md | api-resources.md | routing.md | response-helper.md | middleware.md | authentication.md | service-provider.md


TL;DR (5초 요약)

1. DB 접근 전 .env 파일 존재 확인 필수
2. 테이블 존재 여부 Schema::hasTable() 체크
3. 인스톨러 안정성: 마이그레이션 전에도 부팅 가능해야 함
4. 조건 미충족 시 예외 대신 안전하게 스킵
5. runningInConsole() + 'migrate' 명령 감지로 스킵
6. 성능 최적화: config('app.installer_completed') 가드로 설치 완료 환경에서 hasTable 호출 제거

목차

  1. 핵심 원칙
  2. 검증이 필요한 경우
  3. 검증 체크리스트
  4. 잘못된 예시
  5. 올바른 예시
  6. 실행 흐름 다이어그램
  7. 다른 서비스 프로바이더 적용 예시
  8. 서비스 프로바이더 개발 체크리스트
  9. 인스톨러 테스트 시나리오

핵심 원칙

필수: 서비스 프로바이더에서 DB 접근 전 .env + 테이블 존재 확인
필수: .env 파일 및 테이블 존재 여부 확인 후 접근

핵심 원칙:

  • 인스톨러 안정성: .env 파일이 없는 상태에서도 composer install 실행 가능해야 함
  • 단계적 초기화: DB 테이블이 없는 상태(마이그레이션 전)에서도 애플리케이션 부팅 가능해야 함
  • 안전한 스킵: 조건 미충족 시 예외 발생 대신 안전하게 건너뛰기

검증이 필요한 경우

상황 검증 항목 필요 이유
DB 쿼리 실행 .env 파일, 테이블 존재 인스톨러 실행 전 오류 방지
모듈/플러그인 로드 관련 테이블 존재 마이그레이션 전 오류 방지
설정값 로드 .env 파일 존재 초기 설정 전 오류 방지

검증 체크리스트

  1. .env 파일 존재 여부: File::exists(base_path('.env'))
  2. 필수 테이블 존재 여부: Schema::hasTable('table_name')
  3. 조건 미충족 시 안전하게 return

잘못된 예시

// ❌ 검증 없이 DB 접근 - 인스톨러 실행 시 오류 발생
class ModuleRouteServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->routes(function () {
            $this->loadModuleRoutes();
        });
    }

    protected function loadModuleRoutes(): void
    {
        $modulesPath = base_path('modules');

        if (! File::exists($modulesPath)) {
            return;
        }

        // ❌ .env 파일이나 테이블 확인 없이 DB 쿼리 실행
        $activeModules = Module::where('status', 'active')->get();

        foreach ($activeModules as $module) {
            // 라우트 로드...
        }
    }
}

오류 시나리오:

# 인스톨러 실행 중
composer install
↓
package:discover 자동 실행
↓
ModuleRouteServiceProvider 로드
↓
Module::where() 실행 시도
↓
❌ SQLSTATE[HY000] [1045] Access denied (using password: NO)

올바른 예시

1. .env 파일 체크 추가

// ✅ .env 파일 및 테이블 존재 확인
class ModuleRouteServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->routes(function () {
            $this->loadModuleRoutes();
        });
    }

    protected function loadModuleRoutes(): void
    {
        // ✅ .env 파일이 없으면 스킵 (인스톨러 실행 전)
        if (! File::exists(base_path('.env'))) {
            return;
        }

        $modulesPath = base_path('modules');

        if (! File::exists($modulesPath)) {
            return;
        }

        // ✅ 데이터베이스 테이블이 존재하지 않으면 스킵 (마이그레이션 전)
        if (! Schema::hasTable('modules')) {
            return;
        }

        // 안전하게 DB 접근
        $activeModules = Module::where('status', ExtensionStatus::Active->value)
            ->pluck('identifier')
            ->toArray();

        foreach ($activeModules as $moduleIdentifier) {
            // 라우트 로드...
        }
    }
}

2. Service 클래스에서의 적용

class TemplateService
{
    /**
     * 활성화된 모듈의 routes 데이터 로드
     */
    private function loadActiveModulesRoutesData(): array
    {
        // ✅ .env 파일이 없으면 빈 배열 반환 (인스톨러 실행 전)
        if (! file_exists(base_path('.env'))) {
            return [];
        }

        // ✅ modules 테이블이 없으면 빈 배열 반환 (마이그레이션 전)
        if (! Schema::hasTable('modules')) {
            return [];
        }

        // 안전하게 모듈 데이터 로드
        $routes = [];
        $activeModules = $this->moduleManager->getActiveModules();

        foreach ($activeModules as $module) {
            // routes.json 로드...
        }

        return $routes;
    }
}

실행 흐름 다이어그램

인스톨러 단계 1: composer install
├─ .env 없음 → ModuleRouteServiceProvider 스킵 ✅
├─ vendor 재사용 분기 (vendor/autoload.php + composer.lock 존재)
│  ├─ 개발용 패키지 감지 → 경고 카드·설치 로그 (설치는 계속)
│  └─ 이전 환경의 컴파일 캐시 정리 (packages/services/config)
└─ package:discover 정상 완료

인스톨러 단계 2: .env 생성
├─ DB 연결 정보 입력
└─ .env 파일 생성 완료

인스톨러 단계 3: 마이그레이션
├─ modules 테이블 없음 → ModuleRouteServiceProvider 스킵 ✅
└─ 테이블 생성 완료

설치 완료 후:
├─ .env 있음 ✅
├─ modules 테이블 있음 ✅
└─ ModuleRouteServiceProvider 정상 실행 ✅

다른 서비스 프로바이더 적용 예시

PluginRouteServiceProvider

class PluginRouteServiceProvider extends ServiceProvider
{
    protected function loadPluginRoutes(): void
    {
        // ✅ 동일한 패턴 적용
        if (! File::exists(base_path('.env'))) {
            return;
        }

        if (! Schema::hasTable('plugins')) {
            return;
        }

        // 플러그인 라우트 로드...
    }
}

ConfigServiceProvider

class ConfigServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // ✅ 동일한 패턴 적용
        if (! File::exists(base_path('.env'))) {
            return;
        }

        if (! Schema::hasTable('settings')) {
            return;
        }

        // 동적 설정 로드...
    }
}

성능 최적화: installer_completed 가드

필수: 인스톨러 안전 체크(hasTable 폴백) 는 유지하되,
      프로덕션(설치 완료) 환경에서는 `config('app.installer_completed')` 가드로
      매 요청 Schema::hasTable() 호출을 건너뛴다.

배경

Schema::hasTable() 은 information_schema.tables 에 대한 DB 쿼리를 실행합니다. 매 HTTP 요청마다 여러 ServiceProvider 와 확장 Trait 에서 이 체크가 반복되면 수십 ms 의 누적 오버헤드가 발생합니다. 설치가 완료된 프로덕션 환경에서는 테이블 존재가 앱 수명주기 동안 불변 이므로 이 체크 자체가 불필요합니다.

구현 패턴

.env 의 INSTALLER_COMPLETED=true 플래그를 config/app.php 에 노출하여 사용합니다:

// config/app.php
'installer_completed' => env('INSTALLER_COMPLETED', false),
// ❌ 매 요청 DB 쿼리 (개선 전)
protected function loadModuleRoutes(): void
{
    if (! File::exists(base_path('.env'))) {
        return;
    }

    try {
        if (! Schema::hasTable('modules')) {
            return;
        }
        if (! Schema::hasColumn('modules', 'identifier')) {
            return;
        }
    } catch (\Exception) {
        return;
    }

    // 라우트 로드...
}
// ✅ installer_completed 가드 적용 (개선 후)
protected function loadModuleRoutes(): void
{
    if (! File::exists(base_path('.env'))) {
        return;
    }

    // 설치 완료 상태에서는 Schema introspection 을 건너뜀 (매 요청 쿼리 제거).
    // 인스톨러 이전 환경에서는 기존 체크 경로로 폴백.
    if (! config('app.installer_completed')) {
        try {
            if (! Schema::hasTable('modules')) {
                return;
            }
            if (! Schema::hasColumn('modules', 'identifier')) {
                return;
            }
        } catch (\Exception) {
            return;
        }
    }

    // 라우트 로드...
}

동작 표

환경 installer_completed 마이그레이션 명령 동작
프로덕션 (설치 완료, 웹 요청) true 아님 가드 통과 → hasTable 스킵 (쿼리 0건)
설치 완료 .env 복사 + 빈 DB (마이그레이션 전) true 실행 중 fast-path 무력화 → hasTable 폴백 (table not found 부팅 실패 방지)
인스톨러 실행 중 / 마이그레이션 전 false (기본값) — 기존 hasTable 폴백 경로 (원본 동작 보존)
테스트 (.env.testing 에 플래그 없음) false — 기존 hasTable 경로

두 번째 행이 이 가드의 핵심 케이스다. INSTALLER_COMPLETED=true 가 적힌 .env 를 빈 DB 새 서버에 복사한 뒤 php artisan migrate 로 테이블을 만들기 전 에 앱이 부팅되면, fast-path 가 테이블 존재를 잘못 전제하여 뒤따르는 쿼리가 "table not found" 로 부팅을 깨뜨린다. 이 컨텍스트에서는 fast-path 를 신뢰하지 않고 실제 hasTable 검증 경로로 폴백해야 한다 (아래 "마이그레이션 컨텍스트 가드" 참조).

적용 가이드

  • .env 파일 체크는 반드시 유지 — 인스톨러가 아직 .env 를 생성하지 않은 시점을 커버
  • hasTable 폴백 경로는 반드시 유지 — installer_completed=false 환경에서도 안전하게 부팅되어야 함
  • try/catch 도 그대로 유지 — DB 연결 실패 시 안전한 스킵 계약 준수
  • 가드는 hasTable 체크 전체 블록을 if (! config('app.installer_completed')) 로 래핑 하는 형태
  • fast-path 이후 쿼리가 try/catch 로 보호되지 않는 지점은 마이그레이션 컨텍스트 가드를 함께 적용 (아래 참조). 예외로 보호되는 지점은 빈 DB 에서도 안전하므로 불필요

마이그레이션 컨텍스트 가드

installer_completed fast-path 는 테이블 존재를 전제한다. 그러나 INSTALLER_COMPLETED=true 가 적힌 .env 를 빈 DB 새 서버에 복사한 뒤 php artisan migrate 로 테이블을 만들기 전 에 앱이 부팅되면, fast-path 가 true 를 반환하고 곧이어 실제 쿼리(Model::where()->pluck() 등)가 실행되어 "table not found" 로 부팅이 실패한다. 마이그레이션을 실행하려는 부팅에서 그 명령이 오히려 실패하는 닭-달걀 상태다.

이를 막기 위해, 스키마를 파괴/재생성하는 마이그레이션 계열 콘솔 명령 판정을 App\Support\InstallerContext::isSchemaMutatingCommand() 로 단일 SSoT 화하고, fast-path 지점에 가드로 결합한다. 명령 리스트: migrate, migrate:fresh, migrate:refresh, migrate:rollback, migrate:reset, db:wipe.

// App\Support\InstallerContext
public static function isSchemaMutatingCommand(): bool
{
    if (! app()->runningInConsole()) {
        return false;
    }

    return in_array(
        $_SERVER['argv'][1] ?? null,
        ['migrate', 'migrate:fresh', 'migrate:refresh', 'migrate:rollback', 'migrate:reset', 'db:wipe'],
        true
    );
}

가드 결합 방식은 지점의 논리 극성 에 따라 다르다:

  • Trait (fast-path 통과 조건) — && 로 결합. 마이그레이션 중이면 fast-path 를 건너뛰고 hasTable 폴백으로 진입.

    if (! InstallerContext::isSchemaMutatingCommand() && config('app.installer_completed')) {
        return true;
    }
    
  • RouteServiceProvider (hasTable 검증 블록 진입 조건) — || 로 결합. 마이그레이션 중이면 hasTable 검증 경로로 진입해 빈 DB 시 early return → 무방비 pluck 도달 차단.

    if (! config('app.installer_completed') || InstallerContext::isSchemaMutatingCommand()) {
        try {
            if (! Schema::hasTable('modules')) {
                return;
            }
            // ...
        } catch (\Exception) {
            return;
        }
    }
    

isRegistryReady() 처럼 fast-path 앞에서 조기 반환하는 지점은 if (InstallerContext::isSchemaMutatingCommand()) { return false; } 로 단순 게이트한다.

부류 A (가드 필수) vs 부류 B (불필요)

부류 조건 예 조치
A fast-path 이후 쿼리가 try/catch 로 미보호 → 빈 DB 에서 부팅 실패 Caches{Module,Plugin,Template}Status, Module/PluginRouteServiceProvider 마이그레이션 컨텍스트 가드 적용
B fast-path 이후 쿼리가 try/catch(\Throwable) 로 보호 → 예외를 삼켜 안전한 기본값 반환 NotificationHookListener, IdentityPolicyRepository::{listHookTargets,activeExtensionIdentifiers} 가드 불필요 (예외 흡수로 안전)

config 캐시 재생성 주의

  • config:cache 는 config 소스를 변경하는 라이프사이클에서 App\Support\ConfigCacheHelper::rebuild() 로 자동 재생성된다 (아래 "config 캐시 자동 재생성" 참조). .env 를 직접 편집한 경우처럼 헬퍼를 거치지 않는 변경은 여전히 php artisan config:clear && php artisan config:cache 를 수동 실행한다
  • .env 우선 모드(G7_ENV_PRIORITY)에서는 이 수동 재실행이 기능의 일부다 — .env 의 키 명시 여부는 config 빌드 시점에 config/env-priority.php 로 캡처되므로, .env 만 고치고 재빌드하지 않으면 잠금 상태가 갱신되지 않는다. 런타임 env() 로 명시 여부를 판별하면 config:cache 환경에서 항상 null 이라 판정이 영구 미발동한다. 상세: admin-settings-access.md

확장 Trait 패턴

CachesModuleStatus::isExtensionTableReady(), CachesPluginStatus::isPluginTableReady(), CachesTemplateStatus::isTemplateTableReady() 등 확장 Trait 에도 동일 가드를 적용합니다:

private static function isExtensionTableReady(string $table): bool
{
    if (config('app.installer_completed')) {
        return true;
    }

    try {
        DB::connection()->getPdo();

        return Schema::hasTable($table);
    } catch (\Throwable $e) {
        return false;
    }
}

부팅 경로 Listener / Repository 적용

ServiceProvider 와 Trait 외에도, CoreServiceProvider::boot() 말미(동적 훅 등록 / IDV 정책 동기화)에서 매 요청 실행되는 다음 지점도 동일 가드를 적용합니다:

  • NotificationHookListener::registerDynamicHooks() — notification_definitions 존재 확인 (단일 최대 비용)
  • IdentityPolicyRepository::listHookTargets() — identity_policies 존재 확인
  • IdentityPolicyRepository::activeExtensionIdentifiers() — modules / plugins 존재 확인 (테이블 부재 시 null 반환으로 필터 미적용 계약 보존)
// hasTable 을 단독 조건이 아니라 installer_completed 와 && 로 단락.
// 설치 완료 시 hasTable 호출 자체를 건너뛰고 후속 로직으로 진행한다.
if (! config('app.installer_completed') && ! Schema::hasTable('notification_definitions')) {
    return;
}

try/catch 블록 안에 있는 Repository 조회는 catch 계약(테이블 부재/DB 오류 시 [] 또는 null 반환)을 그대로 보존한 채 조건만 확장한다.

부팅 경로 정적 훅 등록 캐시

installer_completed 가드가 부팅 시 DB 조회를 줄이는 것과 별개로, CoreServiceProvider::boot() 의 정적 훅 리스너 등록(app/Listeners 재귀 스캔 + 모듈/플러그인 getHookListeners() 클래스 로딩)은 bootstrap/cache/hooks.php 캐시로 매 요청 스캔·리플렉션을 제거한다. 캐시 부재/테스트 환경에서는 기존 스캔 경로로 안전 폴백하며, 등록 결과는 스캔 경로와 바이트 동일하다. 상세: extension/hooks.md "정적 훅 매핑 캐시".

주의사항

  • 인스톨러 설치 완료 시점에 INSTALLER_COMPLETED=true 가 .env 에 기록되어야 함 — G7 인스톨러(public/install/includes/task-runner.php)는 이를 이미 수행
  • 가드는 성능 최적화이며 안전 체크 대체가 아님 — 하드웨어 장애 등으로 테이블이 소실된 경우를 커버하지 못함. 그런 상황은 별도 헬스체크로 처리
  • 같은 HTTP 요청 내에서 테이블을 생성/삭제하는 플로우(예: 설치 직후 검증)가 있다면 가드가 stale 결과를 줄 수 있으므로 그 경로에서는 직접 Schema::hasTable() 호출 권장

서비스 프로바이더 개발 체크리스트

  • DB 접근이 필요한 경우 .env 파일 존재 확인
  • 특정 테이블 접근 시 Schema::hasTable() 확인
  • 조건 미충족 시 예외 발생 대신 return으로 안전하게 스킵
  • config('app.installer_completed') 가드로 hasTable 블록 래핑 (성능 최적화)
  • 인스톨러 환경에서 테스트 수행 (INSTALLER_COMPLETED 미설정 상태)
  • composer install 단독 실행 테스트

인스톨러 테스트 시나리오

# 1. .env 없이 composer install
rm .env
composer install
# 예상: 정상 완료 (오류 없음)

# 2. .env 생성 후 애플리케이션 부팅
cp .env.example .env
php artisan config:clear
# 예상: 정상 부팅 (modules 테이블 없어도 오류 없음)

# 3. 마이그레이션 후 정상 동작
php artisan migrate
# 예상: 모든 기능 정상 동작

config 캐시 자동 재생성 (ConfigCacheHelper)

config 소스(config/*.php, .env, storage/app/settings/*.json, 활성 확장 목록)를 변경하는 라이프사이클은 App\Support\ConfigCacheHelper::rebuild() 를 호출해 config 캐시를 재빌드한다.

배경

과거 설정 변경/코어 업데이트/APP_KEY 재생성 등은 config:clear 만 하고 config:cache 를 재생성하지 않았다. 그 결과 관리자가 "시스템 최적화"(optimizeSystem)로 캐시를 만들어도, 이후 설정을 한 번 저장하면 캐시가 비워진 채 재생성되지 않아 이후 모든 요청이 config 파일을 재파싱했다(성능 손실). rebuild() 는 clear 직후 재생성까지 수행해 캐시가 비활성 상태로 잔존하지 않게 한다.

정책

  • 환경 무관 항상 재생성 — config:cache 는 그 자체로 부팅 비용을 줄이고, G7 설정은 config 캐시에 박제되지 않고 매 요청 런타임 Config::set() 으로 재주입되므로(설정 stale 없음) 항상 켜두는 것이 이득이다.
  • testing 환경: config:clear 는 수행(값 반영 계약 유지)하되 config:cache 생성은 스킵(캐시된 config 가 다음 테스트로 누출되는 격리 파괴 방지).
  • 설치 미완료 상태: 불완전 config 박제를 피하려 재생성을 스킵하고 clear 만 한다.
  • local 개발 주의: config:cache 가 켜지면 config/*.php 를 직접 수정해도 다음 요청에 반영되지 않는다. 이는 개발자가 php artisan config:clear 로 대응하는 개발자 책임 영역이다.

적용 지점

라이프사이클 재생성 위치
코어 설치 완료 인스톨러 complete_flag 직후 config:cache task
코어 업데이트 CoreUpdateCommand Step 11 완료 지점
코어 업그레이드 스텝(단독 실행) ExecuteUpgradeStepsCommand 캐시 정리 블록 (spawn 자식은 부모가 처리)
코어 관리자 설정 저장 / 캐시 정리 / GeoIP / APP_KEY SettingsService / GeoIpDatabaseService 의 각 지점
확장 설치/삭제/업데이트 ExtensionManager::updateComposerAutoload() (오토로드·훅 캐시와 동일 생명주기)
확장 활성화/비활성화 ExtensionConfigCacheListener (core.*.activated / core.*.after_deactivate 훅 구독)

ExtensionConfigCacheListener 의 구독 훅은 getSubscribedHooks() 에서 'sync' => true 로 선언한다. 훅 Action 리스너의 기본 등록 정책은 큐 디스패치(HookListenerRegistrar)인데, config 캐시 재생성을 큐로 미루면 (1) 워커 미가동 환경에서 영영 재생성되지 않고, (2) 확장 토글을 수행하는 CLI 커맨드(plugin:deactivate 등)는 실행 후 프로세스가 종료되어 큐 작업을 처리할 주체가 없다. config 캐시 재생성은 인프라 부수효과이므로 반드시 동기 실행한다. 회귀 방지 테스트: ExtensionConfigCacheListenerTest::test_all_hooks_are_synchronous.

확장별 개별 환경설정 저장(board/ecommerce/plugin SettingsService)은 재생성 대상이 아니다. 확장 설정은 config/*.php 가 아니라 settings JSON 에 저장되고, 매 요청 CoreServiceProvider::boot 의 loadModule/PluginSettingsToConfig 가 런타임 Config::set('g7_settings.*', 최신값) 으로 config 캐시에 박제된 값을 덮어쓴다. 따라서 값 stale 이 없고(즉시 반영), config 캐시를 clear 하지도 않으므로(성능 손실 없음) 재생성이 실효가 없다.


관련 이슈

  • 인스톨러에서 composer install 시 DB 접근 오류
  • 마이그레이션 전 애플리케이션 부팅 실패
  • CI/CD 파이프라인에서 의존성 설치 오류

관련 문서