메인 콘텐츠로 건너뛰기

시작하며

Laravel 9는 2022년 2월 8일에 릴리스되었습니다. 이 가이드에서는 Laravel 8.x에서 9.x로의 업그레이드 절차와 영향이 큰 변경점을 정리합니다.
업그레이드에 필요한 예상 시간은 약 30분입니다. 다만 메일 송신 · 파일 스토리지 · 독자 캐스트 · 프레임워크의 코어 클래스의 오버라이드 상황에 따라 작업량은 늘어날 수 있습니다.

Laravel Shift를 사용한 자동 업그레이드

Laravel Shift를 사용하여 업그레이드를 자동화할 수도 있습니다. Shift는 composer.json이나 설정 파일의 갱신을 보조해 주므로, 차분 확인의 출발점으로서 편리합니다.

영향도별 변경점

영향도: 높음

  • 의존관계 갱신
  • Flysystem 3.x로의 마이그레이션
  • Symfony Mailer로의 마이그레이션

영향도: 중간

  • BelongsToManyfirstOrNew / firstOrCreate / updateOrCreate 메서드
  • Custom Casts와 null의 동작
  • HTTP 클라이언트의 기본 타임아웃
  • PHP Return Types 추가
  • Postgres의 schema 설정명 변경
  • assertDeleted 메서드의 폐지
  • lang 디렉터리 이동
  • 패스워드 룰 변경
  • when / unless 메서드의 변경
  • 유효성 검증에서의 미검증 배열 키의 취급

업그레이드 절차

의존관계 갱신

영향도: 높음 Laravel 9에서는 PHP 8.0.2 이상이 필요합니다. 우선 composer.json의 의존관계를 재검토해 주세요.
{
  "require": {
    "php": "^8.0.2",
    "laravel/framework": "^9.0",
    "spatie/laravel-ignition": "^1.0"
  },
  "require-dev": {
    "nunomaduro/collision": "^6.1"
  }
}
추가로, 해당하는 애플리케이션에서는 다음 갱신도 필요합니다.
  • facade/ignition을 삭제하고, spatie/laravel-ignition:^1.0으로 대체
  • pusher/pusher-php-server를 사용하고 있는 경우는 ^5.0으로 갱신
  • 이용 중인 서드파티 패키지가 Laravel 9 대응판인지 확인
  • Vonage 통지 채널을 사용하고 있는 경우는 개별 업그레이드 가이드도 확인
갱신 후에는 의존관계를 설치합니다.
composer update

PHP 버전 요구사항

영향도: 높음 Laravel 9에서는 PHP 8.0.2 이상이 필수입니다. CI, 로컬 개발 환경, 프로덕션 환경 모두에서 PHP 버전을 맞추고 나서 업그레이드를 진행해 주세요.

Symfony Mailer로의 마이그레이션

영향도: 높음 Laravel 9의 큰 변경 중 하나는 2021년 12월로 유지보수가 종료된 SwiftMailer에서 Symfony Mailer로의 마이그레이션입니다. 일반적인 Mail::to()->send()만 사용하고 있는 애플리케이션은 영향이 적은 한편, SwiftMailer의 저레벨 API를 직접 만지고 있는 경우에는 확인이 필요합니다.

드라이버 의존관계

# Mailgun을 사용하는 경우에만 추가
composer require symfony/mailgun-mailer symfony/http-client

# Postmark를 사용하는 경우는 SwiftMailer용 패키지를 삭제하고 대체
composer remove wildbit/swiftmailer-postmark
composer require symfony/postmark-mailer symfony/http-client

withSwiftMessage에서 withSymfonyMessage

// Laravel 8.x: SwiftMailer 기반
$this->withSwiftMessage(function ($message) {
    $message->getHeaders()->addTextHeader('Custom-Header', 'Header Value');
});

// Laravel 9.x: Symfony Mailer 기반
use Symfony\Component\Mime\Email;

$this->withSymfonyMessage(function (Email $message) {
    $message->getHeaders()->addTextHeader('Custom-Header', 'Header Value');
});
Illuminate\Mail\Mailersend, html, raw, plainvoid가 아니라 Illuminate\Mail\SentMessage를 반환하도록 되었습니다. 또한 MessageSent 이벤트의 message 프로퍼티에는 Swift_Message가 아니라 Symfony\Component\Mime\Email이 들어갑니다.

SMTP 설정의 재검토

Symfony Mailer에서는 SMTP의 stream 옵션이 폐지되고, 지원되는 설정은 톱 레벨로 이동합니다.
return [
    'mailers' => [
        'smtp' => [
            // Laravel 8.x 설정
            'stream' => [
                'ssl' => [
                    'verify_peer' => false,
                ],
            ],

            // Laravel 9.x 설정
            'verify_peer' => false,
        ],
    ],
];
auth_mode의 명시적 설정도 불필요해졌습니다. 무효한 메일 주소를 송신 후에 회수하는 것이 아니라, 송신 전에 유효성 검증하는 운용으로 옮기는 것이 안전합니다.

Flysystem 3.x로의 마이그레이션

영향도: 높음 Laravel 9는 Storage 파사드의 내부 구현을 Flysystem 1.x에서 3.x로 갱신했습니다. 파일 조작 메서드는 가능한 한 호환성을 유지하고 있지만, 예외 · 반환값 · 어댑터 등록 주변에 차분이 있습니다.

드라이버의 추가 설치

# Amazon S3 드라이버
composer require -W league/flysystem-aws-s3-v3 "^3.0"

# FTP 드라이버
composer require league/flysystem-ftp "^3.0"

# SFTP 드라이버
composer require league/flysystem-sftp-v3 "^3.0"

Storage의 주요 동작 변경

  • put / write / writeStream은 기존 파일을 기본으로 덮어쓰기
  • 쓰기 실패 시는 예외가 아니라 false를 반환
  • 존재하지 않는 파일의 읽기는 예외가 아니라 null을 반환
  • 존재하지 않는 파일의 deletetrue를 반환
  • cached adapter는 삭제되었으므로, disk 설정 내의 cache 키를 삭제할 수 있음
이전처럼 쓰기 실패 시에 예외를 사용하고 싶은 경우에는, throw 옵션을 설정해 주세요.
return [
    'disks' => [
        'public' => [
            'driver' => 'local',
            // 쓰기 실패 시에 예외를 던지고 싶은 경우에만 유효로
            'throw' => true,
        ],
    ],
];
독자적인 filesystem 드라이버를 등록하고 있는 경우에는, Storage::extend()의 콜백이 Illuminate\Filesystem\FilesystemAdapter를 직접 반환하는 구현으로 조정해 주세요.

BelongsToManyfirstOrNew / firstOrCreate / updateOrCreate

영향도: 중간 Laravel 8에서는 이 메서드들에 전달한 제1인자의 속성 배열이 중간 테이블과 비교되고 있었습니다. Laravel 9에서는 관련 모델의 테이블과 비교됩니다.
// 관련 모델의 테이블에 대해 name을 검색 · 갱신
$user->roles()->updateOrCreate([
    'name' => 'Administrator',
]);
또한 firstOrCreate는 제2인자의 $values를 받을 수 있게 되어, 다른 릴레이션과 동작이 맞춰졌습니다.
// 신규 생성 시에만 created_by를 병합
$user->roles()->firstOrCreate([
    'name' => 'Administrator',
], [
    'created_by' => $user->id,
]);

Custom Casts와 null

영향도: 중간 Laravel 9에서는 캐스트 대상에 null을 대입한 경우에도 custom cast의 set 메서드가 호출됩니다. null을 상정하지 않은 cast는 업그레이드 후에 예외를 낼 가능성이 있습니다.
public function set($model, $key, $value, $attributes)
{
    // Laravel 9에서는 null이 전달될 가능성이 있다
    if ($value === null) {
        return [
            'address_line_one' => null,
            'address_line_two' => null,
        ];
    }

    if (! $value instanceof AddressModel) {
        throw new InvalidArgumentException('AddressModel을 전달해 주세요.');
    }

    return [
        'address_line_one' => $value->lineOne,
        'address_line_two' => $value->lineTwo,
    ];
}

HTTP 클라이언트의 기본 타임아웃

영향도: 중간 HTTP 클라이언트의 기본 타임아웃은 30초가 되었습니다. 이전에는 무제한으로 계속 대기하는 경우가 있었습니다.
use Illuminate\Support\Facades\Http;

// 오래 걸리는 API 호출만 개별로 타임아웃을 연장
$response = Http::timeout(120)->get('https://example.com/api/status');

PHP Return Types 추가

영향도: 중간 Laravel 9에서는 PHP 본체나 Symfony의 요건에 맞춰 각종 코어 클래스에 반환값 타입이 추가되었습니다. Laravel의 코어 클래스를 상속하여 offsetGet, offsetSet, jsonSerialize, open, read 등을 오버라이드하고 있는 경우에는, 자체 구현에도 같은 반환값 타입을 추가해 주세요.
// 코어 클래스를 오버라이드하고 있는 경우는 반환값 타입을 맞춘다
public function offsetGet($key): mixed
{
    return parent::offsetGet($key);
}

Postgres의 schema 설정명 변경

영향도: 중간 Postgres 접속에서 검색 경로를 설정하고 있는 경우, config/database.php의 키 이름을 schema에서 search_path로 변경해 주세요.
return [
    'pgsql' => [
        // Laravel 8.x
        'schema' => 'public',

        // Laravel 9.x
        'search_path' => 'public',
    ],
];

assertDeleted에서 assertModelMissing으로

영향도: 중간 모델의 삭제 확인에 사용하고 있던 assertDeletedassertModelMissing으로 대체해 주세요.
// 변경 전
$this->assertDeleted($user);

// 변경 후
$this->assertModelMissing($user);

lang 디렉터리 이동

영향도: 중간 신규 Laravel 9 애플리케이션에서는 언어 파일의 배치처가 resources/lang이 아니라 프로젝트 루트의 lang이 되었습니다. 기존 앱을 그대로 동작시키기만 하는 경우에는 큰 영향은 적지만, 새 스켈레톤에 맞추는 경우나 패키지에서 번역 파일을 공개하고 있는 경우에는 재검토해 주세요.
// 고정 경로가 아니라 langPath()를 사용하여 공개처를 결정
$this->publishes([
    __DIR__.'/../lang' => app()->langPath('vendor/package-name'),
]);

패스워드 룰 변경

영향도: 중간 현재 로그인 중인 사용자의 패스워드와 일치하는 것을 검증하는 password 룰은 current_password로 리네임되었습니다.
// 변경 전
'password' => ['required', 'password'],

// 변경 후
'password' => ['required', 'current_password'],

when / unless 메서드의 변경

영향도: 중간 Laravel 8에서는 when이나 unless에 클로저를 전달하면 그 클로저 자체가 truthy로 평가되기 때문에 의도치 않게 조건 분기가 실행되는 경우가 있었습니다. Laravel 9에서는 클로저가 실행되고, 그 반환값이 조건으로 사용됩니다.
$collection->when(function ($collection) {
    // 여기서 반환한 값이 조건식으로서 평가됨
    return false;
}, function ($collection) {
    // false가 반환되었으므로 이 처리는 실행되지 않음
    $collection->merge([1, 2, 3]);
});

미검증 배열 키의 취급

영향도: 중간 Laravel 9에서는 validated()가 반환하는 배열에서 미검증의 배열 키가 항상 제외됩니다. Laravel 8의 호환 동작을 유지하고 싶은 경우에만, includeUnvalidatedArrayKeys()를 명시적으로 호출해 주세요.
use Illuminate\Support\Facades\Validator;

public function boot()
{
    // Laravel 8과 같은 동작으로 되돌리고 싶은 경우에만 유효로
    Validator::includeUnvalidatedArrayKeys();
}

정리

Laravel 8에서 9로의 업그레이드에서는 PHP 8.0.2로의 갱신, Symfony Mailer로의 마이그레이션, Flysystem 3.x로의 대응이 중심입니다. 메일 송신 · 스토리지 · 독자 캐스트 · 테스트 헬퍼를 먼저 점검하면, 업그레이드 후의 불편을 줄일 수 있습니다.
변경점영향도대응
PHP 8.0.2 / laravel/framework:^9.0높음composer.json과 실행 환경 갱신
Symfony Mailer로의 마이그레이션높음SwiftMailer 계 API와 mail 설정 확인
Flysystem 3.x높음Storage의 반환값 · 예외 · 드라이버 의존관계 확인
BelongsToMany의 upsert 계 메서드중간검색 대상이 관련 모델의 테이블이 된 점 확인
Custom Casts와 null중간set()null을 받아도 부서지지 않도록 수정
HTTP 클라이언트의 30초 타임아웃중간필요한 요청만 timeout()을 명시
assertDeleted 폐지중간assertModelMissing()으로 대체
lang 디렉터리 이동중간고정 경로 참조를 app()->langPath()로 변경
password 룰 변경중간current_password로 갱신
미검증 배열 키의 제외중간필요 시에만 includeUnvalidatedArrayKeys() 이용

참고 자료

마지막 수정일 2026년 7월 13일