본문 바로가기

Eloquent 관계

업데이트됨

번역일: 2026년 9월 17일

이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.

원문 수정
2026년 9월 17일
번역 갱신
2026년 9월 17일

Eloquent 관계

소개

데이터베이스 테이블은 서로 연관되어 있는 경우가 대부분입니다. 예를 들어 블로그 게시글에는 여러 개의 댓글이 달릴 수 있고, 하나의 주문은 이를 작성한 사용자와 연결됩니다. Eloquent는 이런 관계를 손쉽게 다루고 활용할 수 있도록 도와주며, 다음과 같은 다양한 종류의 관계를 지원합니다.

NOTE

관계 종류를 처음 접한다면 다소 헷갈릴 수 있습니다. 각 관계가 실제로 언제, 왜 필요한지 이해하고 나면 훨씬 수월해지므로, 아래 예제들을 실제 프로젝트(주문-상품, 게시글-댓글, 사용자-역할 등)에 대입해가며 읽어보시길 권장합니다.

Eloquent 관계는 Eloquent 모델 클래스 안에서 메서드로 정의합니다. 관계 역시 강력한 쿼리 빌더 역할을 겸하기 때문에, 메서드로 관계를 정의하면 체이닝 가능한 쿼리 기능과 함께 매우 강력한 기능을 제공받게 됩니다. 예를 들어 아래처럼 posts 관계에 추가 쿼리 제약 조건을 체이닝할 수 있습니다.

$user->posts()->where('active', 1)->get();

관계를 자세히 다루기 전에, Eloquent가 지원하는 각 관계 유형을 정의하는 방법을 먼저 살펴보겠습니다.

Eloquent 관계

소개

데이터베이스 테이블은 서로 연관되어 있는 경우가 많습니다. 예를 들어 블로그 게시글에는 여러 개의 댓글이 달릴 수 있고, 주문 정보는 이를 생성한 사용자와 연결되어 있을 수 있습니다. Eloquent는 이러한 관계를 손쉽게 정의하고 다룰 수 있도록 해주며, 다음과 같은 다양한 관계 유형을 지원합니다:

NOTE

관계의 종류가 많아 처음에는 어떤 것을 써야 할지 헷갈릴 수 있습니다. 핵심은 "이 관계가 어느 쪽에 얼마나 존재하는가"를 먼저 파악하는 것입니다. 예를 들어 "게시글 하나에 댓글이 여러 개"라면 일대다, "역할(Role) 하나를 여러 사용자가 공유"한다면 다대다를 사용하게 됩니다. 아래 각 절에서 실제 코드와 함께 자세히 살펴보겠습니다.

Eloquent 관계

관계 정의하기

Eloquent 관계는 Eloquent 모델 클래스 안에 메서드 형태로 정의합니다. 관계 역시 강력한 쿼리 빌더 역할을 겸하기 때문에, 관계를 메서드로 정의하면 메서드 체이닝을 통해 쿼리 조건을 자유롭게 덧붙일 수 있다는 장점이 있습니다. 예를 들어 아래처럼 posts 관계에 추가 조건을 체이닝할 수 있습니다.

$user->posts()->where('active', 1)->get();

관계를 본격적으로 활용하기 전에, Eloquent가 지원하는 관계 유형을 하나씩 살펴보겠습니다.

1:1 관계 / Has One

1:1(one-to-one) 관계는 가장 기본적인 데이터베이스 관계 유형입니다. 예를 들어 User 모델 하나는 Phone 모델 하나와 연결될 수 있습니다. 이 관계를 정의하려면 User 모델에 phone 메서드를 추가하면 됩니다. phone 메서드는 hasOne 메서드를 호출하고 그 결과를 반환해야 합니다. hasOne 메서드는 모델의 부모 클래스인 Illuminate\Database\Eloquent\Model에서 제공됩니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasOne; class User extends Model { /** * 이 사용자와 연결된 전화번호를 가져옵니다. */ public function phone(): HasOne { return $this->hasOne(Phone::class); } }

hasOne 메서드의 첫 번째 인자는 연관된 모델 클래스의 이름입니다. 관계를 정의하고 나면, Eloquent의 동적 프로퍼티(dynamic property)를 통해 연관된 레코드를 가져올 수 있습니다. 동적 프로퍼티를 사용하면 관계 메서드를 마치 모델에 정의된 프로퍼티처럼 접근할 수 있습니다.

$phone = User::find(1)->phone;

Eloquent는 부모 모델의 이름을 기준으로 관계의 외래 키(foreign key)를 자동으로 결정합니다. 이 예시에서는 Phone 모델에 user_id라는 외래 키가 있다고 가정합니다. 이 관례를 변경하고 싶다면 hasOne 메서드의 두 번째 인자로 원하는 외래 키명을 전달하면 됩니다.

return $this->hasOne(Phone::class, 'foreign_key');

또한 Eloquent는 외래 키의 값이 부모 모델의 기본 키(primary key) 값과 일치한다고 가정합니다. 즉, Phone 레코드의 user_id 컬럼 값에서 사용자의 id 컬럼 값을 찾게 됩니다. 만약 id가 아닌 다른 컬럼을 기준으로 관계를 맺고 싶다면, hasOne 메서드의 세 번째 인자로 로컬 키(local key)를 지정할 수 있습니다.

return $this->hasOne(Phone::class, 'foreign_key', 'local_key');

역방향 관계 정의하기

지금까지는 User 모델에서 Phone 모델에 접근하는 방법을 살펴봤습니다. 이번에는 반대로 Phone 모델에서 해당 전화번호를 소유한 사용자에게 접근할 수 있도록 관계를 정의해보겠습니다. hasOne 관계의 역방향은 belongsTo 메서드로 정의합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Phone extends Model { /** * 이 전화번호를 소유한 사용자를 가져옵니다. */ public function user(): BelongsTo { return $this->belongsTo(User::class); } }

user 메서드를 호출하면, Eloquent는 Phone 모델의 user_id 컬럼 값과 일치하는 id를 가진 User 모델을 찾으려고 시도합니다.

Eloquent는 관계 메서드의 이름 뒤에 _id를 붙여서 외래 키명을 결정합니다. 따라서 이 예시에서는 Phone 모델에 user_id 컬럼이 있다고 가정합니다. 그러나 Phone 모델의 외래 키가 user_id가 아니라면, belongsTo 메서드의 두 번째 인자로 사용자 정의 키명을 전달할 수 있습니다.

/** * 이 전화번호를 소유한 사용자를 가져옵니다. */ public function user(): BelongsTo { return $this->belongsTo(User::class, 'foreign_key'); }

만약 부모 모델이 id를 기본 키로 사용하지 않거나, 다른 컬럼을 기준으로 연관 모델을 찾고 싶다면, belongsTo 메서드의 세 번째 인자로 부모 테이블의 사용자 정의 키를 지정할 수 있습니다.

/** * 이 전화번호를 소유한 사용자를 가져옵니다. */ public function user(): BelongsTo { return $this->belongsTo(User::class, 'foreign_key', 'owner_key'); }

1:N 관계 / Has Many

1:N(one-to-many) 관계는 하나의 모델이 여러 개의 자식 모델을 갖는 관계를 정의할 때 사용합니다. 예를 들어 게시글(post) 하나에는 무수히 많은 댓글(comment)이 달릴 수 있습니다. 다른 Eloquent 관계와 마찬가지로, 1:N 관계도 모델에 메서드를 정의하는 방식으로 선언합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; class Post extends Model { /** * 이 게시글의 댓글들을 가져옵니다. */ public function comments(): HasMany { return $this->hasMany(Comment::class); } }

Eloquent는 Comment 모델의 적절한 외래 키 컬럼을 자동으로 결정합니다. 관례상 부모 모델 이름을 스네이크 케이스(snake case)로 변환한 뒤 _id를 붙입니다. 즉, 이 예시에서는 Comment 모델의 외래 키 컬럼이 post_id라고 가정합니다.

관계 메서드를 정의했다면, comments 프로퍼티에 접근해서 연관된 댓글들의 컬렉션을 가져올 수 있습니다. 앞서 설명했듯이 Eloquent는 "동적 관계 프로퍼티"를 제공하므로, 관계 메서드를 모델에 정의된 프로퍼티처럼 사용할 수 있습니다.

use App\Models\Post; $comments = Post::find(1)->comments; foreach ($comments as $comment) { // ... }

관계 역시 쿼리 빌더 역할을 하므로, comments 메서드를 호출한 뒤 조건을 계속 체이닝해서 추가 제약을 걸 수 있습니다.

$comment = Post::find(1)->comments() ->where('title', 'foo') ->first();

hasOne 메서드와 마찬가지로, hasMany 메서드에도 추가 인자를 전달해서 외래 키와 로컬 키를 재정의할 수 있습니다.

return $this->hasMany(Comment::class, 'foreign_key'); return $this->hasMany(Comment::class, 'foreign_key', 'local_key');

자식 모델에 부모 모델 자동으로 채우기(Hydration)

Eloquent의 즉시 로딩(eager loading)을 사용하더라도, 자식 모델을 순회하면서 부모 모델에 접근하려고 하면 "N + 1" 쿼리 문제가 발생할 수 있습니다.

$posts = Post::with('comments')->get(); foreach ($posts as $post) { foreach ($post->comments as $comment) { echo $comment->post->title; } }

위 예시에서는 모든 Post 모델에 대해 댓글을 즉시 로딩했음에도 불구하고 "N + 1" 쿼리 문제가 발생합니다. 이는 Eloquent가 각 자식 Comment 모델에 부모 Post 모델을 자동으로 채워주지 않기 때문입니다.

Eloquent가 부모 모델을 자식 모델에 자동으로 채워주길 원한다면, hasMany 관계를 정의할 때 chaperone 메서드를 호출하면 됩니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; class Post extends Model { /** * 이 게시글의 댓글들을 가져옵니다. */ public function comments(): HasMany { return $this->hasMany(Comment::class)->chaperone(); } }

또는 실행 시점에 원할 때만 부모 모델 자동 채우기를 활성화하고 싶다면, 관계를 즉시 로딩할 때 chaperone 메서드를 호출할 수도 있습니다.

use App\Models\Post; $posts = Post::with([ 'comments' => fn ($comments) => $comments->chaperone(), ])->get();

1:N 관계(역방향) / Belongs To

이제 게시글의 모든 댓글에 접근할 수 있게 되었으니, 반대로 댓글에서 자신이 속한 게시글에 접근할 수 있는 관계를 정의해보겠습니다. hasMany 관계의 역방향을 정의하려면, 자식 모델에 belongsTo 메서드를 호출하는 관계 메서드를 정의하면 됩니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Comment extends Model { /** * 이 댓글이 속한 게시글을 가져옵니다. */ public function post(): BelongsTo { return $this->belongsTo(Post::class); } }

관계를 정의했다면, "동적 관계 프로퍼티"인 post를 통해 댓글의 부모 게시글을 가져올 수 있습니다.

use App\Models\Comment; $comment = Comment::find(1); return $comment->post->title;

위 예시에서 Eloquent는 Comment 모델의 post_id 컬럼 값과 일치하는 id를 가진 Post 모델을 찾으려 시도합니다.

Eloquent는 관계 메서드 이름 뒤에 언더스코어(_)와 부모 모델의 기본 키 컬럼명을 붙여서 기본 외래 키명을 결정합니다. 따라서 이 예시에서는 comments 테이블에 있는 Post 모델의 외래 키가 post_id라고 가정합니다.

만약 외래 키가 이 관례를 따르지 않는다면, belongsTo 메서드의 두 번째 인자로 사용자 정의 외래 키명을 전달할 수 있습니다.

/** * 이 댓글이 속한 게시글을 가져옵니다. */ public function post(): BelongsTo { return $this->belongsTo(Post::class, 'foreign_key'); }

부모 모델이 id를 기본 키로 사용하지 않거나, 다른 컬럼으로 연관 모델을 찾고 싶다면, belongsTo 메서드의 세 번째 인자로 부모 테이블의 사용자 정의 키를 지정할 수 있습니다.

/** * 이 댓글이 속한 게시글을 가져옵니다. */ public function post(): BelongsTo { return $this->belongsTo(Post::class, 'foreign_key', 'owner_key'); }

기본 모델(Default Models)

belongsTo, hasOne, hasOneThrough, morphOne 관계는 관계 대상이 null일 때 반환할 기본 모델을 정의할 수 있습니다. 이 패턴은 흔히 널 오브젝트 패턴(Null Object Pattern)이라고 불리며, 코드 내 조건문을 줄이는 데 도움이 됩니다. 아래 예시에서는 Post 모델에 연결된 사용자가 없을 경우 user 관계가 빈 App\Models\User 모델을 반환하도록 합니다.

/** * 이 게시글의 작성자를 가져옵니다. */ public function user(): BelongsTo { return $this->belongsTo(User::class)->withDefault(); }

기본 모델에 속성 값을 채우고 싶다면, withDefault 메서드에 배열이나 클로저를 전달하면 됩니다.

/** * 이 게시글의 작성자를 가져옵니다. */ public function user(): BelongsTo { return $this->belongsTo(User::class)->withDefault([ 'name' => 'Guest Author', ]); } /** * 이 게시글의 작성자를 가져옵니다. */ public function user(): BelongsTo { return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) { $user->name = 'Guest Author'; }); }

Belongs To 관계 쿼리하기

"belongs to" 관계의 자식 모델을 조회할 때, 직접 where 절을 작성해서 해당 Eloquent 모델을 가져올 수 있습니다.

use App\Models\Post; $posts = Post::where('user_id', $user->id)->get();

하지만 whereBelongsTo 메서드를 사용하면 훨씬 편리합니다. 이 메서드는 주어진 모델에 알맞은 관계와 외래 키를 자동으로 파악합니다.

$posts = Post::whereBelongsTo($user)->get();

whereBelongsTo 메서드에는 컬렉션 인스턴스를 전달할 수도 있습니다. 이 경우 Laravel은 컬렉션 내 부모 모델 중 하나에라도 속한 모델들을 모두 조회합니다.

$users = User::where('vip', true)->get(); $posts = Post::whereBelongsTo($users)->get();

기본적으로 Laravel은 전달된 모델의 클래스명을 기반으로 관계를 자동 판별하지만, whereBelongsTo 메서드의 두 번째 인자로 관계명을 직접 지정할 수도 있습니다.

$posts = Post::whereBelongsTo($user, 'author')->get();

여러 개 중 하나만 가져오기(Has One of Many)

경우에 따라 하나의 모델이 여러 연관 모델을 가지고 있지만, 그중 "가장 최근" 또는 "가장 오래된" 연관 모델 하나만 손쉽게 가져오고 싶을 때가 있습니다. 예를 들어 User 모델은 여러 Order 모델과 연관될 수 있지만, 사용자가 가장 최근에 주문한 내역에 편리하게 접근할 방법이 필요할 수 있습니다. 이럴 때는 hasOne 관계 유형과 ofMany 계열 메서드를 함께 사용하면 됩니다.

/** * 사용자의 가장 최근 주문을 가져옵니다. */ public function latestOrder(): HasOne { return $this->hasOne(Order::class)->latestOfMany(); }

마찬가지로, 관계에서 "가장 오래된", 즉 첫 번째 연관 모델을 가져오는 메서드도 정의할 수 있습니다.

/** * 사용자의 가장 오래된 주문을 가져옵니다. */ public function oldestOrder(): HasOne { return $this->hasOne(Order::class)->oldestOfMany(); }

기본적으로 latestOfManyoldestOfMany 메서드는 정렬 가능한 기본 키를 기준으로 최신/최초 연관 모델을 조회합니다. 하지만 때로는 다른 정렬 기준으로 여러 연관 모델 중 하나를 가져오고 싶을 수 있습니다.

예를 들어 ofMany 메서드를 사용하면 사용자가 주문한 것 중 가장 비싼 주문을 가져올 수 있습니다. ofMany 메서드는 첫 번째 인자로 정렬 기준이 될 컬럼명을, 두 번째 인자로 적용할 집계 함수(min 또는 max)를 받습니다.

/** * 사용자의 가장 비싼 주문을 가져옵니다. */ public function largestOrder(): HasOne { return $this->hasOne(Order::class)->ofMany('price', 'max'); }

WARNING

PostgreSQL은 UUID 컬럼에 대해 MAX 함수 실행을 지원하지 않으므로, PostgreSQL에서 UUID 컬럼을 사용하는 경우 "여러 개 중 하나" 관계 기능을 함께 사용할 수 없습니다.

"Many" 관계를 "Has One" 관계로 변환하기

latestOfMany, oldestOfMany, ofMany 메서드로 단일 모델을 조회하려는 경우, 이미 같은 모델에 대한 "has many" 관계가 정의되어 있는 경우가 많습니다. 이럴 때 Laravel은 기존 관계에 one 메서드를 호출하기만 하면 손쉽게 "has one" 관계로 변환할 수 있도록 지원합니다.

/** * 사용자의 주문 목록을 가져옵니다. */ public function orders(): HasMany { return $this->hasMany(Order::class); } /** * 사용자의 가장 비싼 주문을 가져옵니다. */ public function largestOrder(): HasOne { return $this->orders()->one()->ofMany('price', 'max'); }

one 메서드는 HasManyThrough 관계를 HasOneThrough 관계로 변환할 때도 사용할 수 있습니다.

public function latestDeployment(): HasOneThrough { return $this->deployments()->one()->latestOfMany(); }

고급 "여러 개 중 하나" 관계

더 복잡한 "여러 개 중 하나" 관계도 구성할 수 있습니다. 예를 들어 Product 모델은 여러 개의 Price 모델과 연관될 수 있고, 새로운 가격이 등록되더라도 기존 가격 데이터는 시스템에 계속 보존된다고 가정해봅시다. 게다가 published_at 컬럼을 통해 특정 미래 시점부터 적용될 새로운 가격을 미리 등록해둘 수도 있습니다.

정리하면, 발행일(published date)이 미래가 아니면서 가장 최근에 발행된 가격을 조회해야 합니다. 또한 발행일이 동일한 가격이 여러 개 있다면 ID가 가장 큰 가격을 우선해야 합니다. 이를 구현하려면 ofMany 메서드에 최신 가격을 결정할 정렬 기준 컬럼들의 배열을 전달해야 합니다. 그리고 두 번째 인자로 클로저를 전달하면, 발행일에 대한 추가 조건을 관계 쿼리에 덧붙일 수 있습니다.

/** * 이 상품의 현재 가격을 가져옵니다. */ public function currentPricing(): HasOne { return $this->hasOne(Price::class)->ofMany([ 'published_at' => 'max', 'id' => 'max', ], function (Builder $query) { $query->where('published_at', '<', now()); }); }

Has One Through

"has-one-through" 관계는 다른 모델과의 1:1 관계를 정의하지만, 두 모델이 직접 연결되어 있지 않고 세 번째 모델을 거쳐서 연결된다는 점이 특징입니다.

예를 들어 자동차 정비소를 관리하는 애플리케이션에서, 각 Mechanic(정비사) 모델은 하나의 Car(자동차) 모델과 연관되고, 각 Car 모델은 다시 하나의 Owner(차주) 모델과 연관될 수 있습니다. 정비사와 차주는 데이터베이스상에서 직접적인 관계가 없지만, 정비사는 Car 모델을 거쳐 차주에게 접근할 수 있습니다. 이 관계를 정의하는 데 필요한 테이블 구조를 살펴보겠습니다.

mechanics
    id - integer
    name - string

cars
    id - integer
    model - string
    mechanic_id - integer

owners
    id - integer
    name - string
    car_id - integer

테이블 구조를 확인했으니, 이제 Mechanic 모델에 관계를 정의해보겠습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasOneThrough; class Mechanic extends Model { /** * 이 정비사가 담당한 자동차의 차주를 가져옵니다. */ public function carOwner(): HasOneThrough { return $this->hasOneThrough(Owner::class, Car::class); } }

hasOneThrough 메서드의 첫 번째 인자는 최종적으로 접근하고자 하는 모델의 이름이고, 두 번째 인자는 중간 모델의 이름입니다.

또는, 관계에 관련된 모든 모델에 이미 필요한 관계가 정의되어 있다면, through 메서드를 호출하고 해당 관계 이름들을 전달하는 방식으로 유연하게 "has-one-through" 관계를 정의할 수도 있습니다. 예를 들어 Mechanic 모델에 cars 관계가, Car 모델에 owner 관계가 이미 정의되어 있다면, 다음과 같이 정비사와 차주를 연결하는 "has-one-through" 관계를 정의할 수 있습니다.

// 문자열 기반 문법... return $this->through('cars')->has('owner'); // 동적 문법... return $this->throughCars()->hasOwner();

키 관례

이 관계의 쿼리를 실행할 때도 일반적인 Eloquent 외래 키 관례가 적용됩니다. 관계의 키를 직접 지정하고 싶다면, hasOneThrough 메서드의 세 번째와 네 번째 인자로 전달하면 됩니다. 세 번째 인자는 중간 모델의 외래 키명이고, 네 번째 인자는 최종 모델의 외래 키명입니다. 다섯 번째 인자는 로컬 키이고, 여섯 번째 인자는 중간 모델의 로컬 키입니다.

class Mechanic extends Model { /** * 이 정비사가 담당한 자동차의 차주를 가져옵니다. */ public function carOwner(): HasOneThrough { return $this->hasOneThrough( Owner::class, Car::class, 'mechanic_id', // cars 테이블의 외래 키... 'car_id', // owners 테이블의 외래 키... 'id', // mechanics 테이블의 로컬 키... 'id' // cars 테이블의 로컬 키... ); } }

또는 앞서 설명한 것처럼, 관계에 관련된 모든 모델에 이미 필요한 관계가 정의되어 있다면, through 메서드를 호출하고 관계 이름들을 전달하는 방식으로 "has-one-through" 관계를 유연하게 정의할 수 있습니다. 이 방식은 기존 관계에 이미 정의된 키 관례를 그대로 재사용할 수 있다는 장점이 있습니다.

// 문자열 기반 문법... return $this->through('cars')->has('owner'); // 동적 문법... return $this->throughCars()->hasOwner();

Has Many Through

"has-many-through" 관계는 중간 관계를 거쳐서 멀리 떨어진 연관 모델에 편리하게 접근할 수 있게 해줍니다. 예를 들어 Laravel Cloud와 같은 배포 플랫폼을 만든다고 가정해봅시다. Application(애플리케이션) 모델은 중간에 있는 Environment(환경) 모델을 거쳐서 여러 Deployment(배포) 모델에 접근할 수 있습니다. 이 예시를 활용하면 특정 애플리케이션의 모든 배포 기록을 손쉽게 가져올 수 있습니다. 이 관계를 정의하는 데 필요한 테이블 구조를 살펴보겠습니다.

applications
    id - integer
    name - string

environments
    id - integer
    application_id - integer
    name - string

deployments
    id - integer
    environment_id - integer
    commit_hash - string

테이블 구조를 확인했으니, 이제 Application 모델에 관계를 정의해보겠습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasManyThrough; class Application extends Model { /** * 이 애플리케이션의 모든 배포 기록을 가져옵니다. */ public function deployments(): HasManyThrough { return $this->hasManyThrough(Deployment::class, Environment::class); } }

hasManyThrough 메서드의 첫 번째 인자는 최종적으로 접근하고자 하는 모델의 이름이고, 두 번째 인자는 중간 모델의 이름입니다.

또는, 관계에 관련된 모든 모델에 이미 필요한 관계가 정의되어 있다면, through 메서드를 호출하고 해당 관계 이름들을 전달하는 방식으로 유연하게 "has-many-through" 관계를 정의할 수도 있습니다. 예를 들어 Application 모델에 environments 관계가, Environment 모델에 deployments 관계가 이미 정의되어 있다면, 다음과 같이 애플리케이션과 배포 기록을 연결하는 "has-many-through" 관계를 정의할 수 있습니다.

// 문자열 기반 문법... return $this->through('environments')->has('deployments'); // 동적 문법... return $this->throughEnvironments()->hasDeployments();

Deployment 모델의 테이블에는 application_id 컬럼이 없지만, hasManyThrough 관계 덕분에 $application->deployments로 애플리케이션의 배포 기록에 접근할 수 있습니다. 이 모델들을 조회할 때, Eloquent는 중간 모델인 Environment 테이블의 application_id 컬럼을 먼저 확인합니다. 관련된 환경(environment) ID들을 찾은 뒤, 이 ID들을 사용해서 Deployment 테이블을 조회하는 방식입니다.

키 관례

이 관계의 쿼리를 실행할 때도 일반적인 Eloquent 외래 키 관례가 적용됩니다. 관계의 키를 직접 지정하고 싶다면, hasManyThrough 메서드의 세 번째와 네 번째 인자로 전달하면 됩니다. 세 번째 인자는 중간 모델의 외래 키명이고, 네 번째 인자는 최종 모델의 외래 키명입니다. 다섯 번째 인자는 로컬 키이고, 여섯 번째 인자는 중간 모델의 로컬 키입니다.

class Application extends Model { public function deployments(): HasManyThrough { return $this->hasManyThrough( Deployment::class, Environment::class, 'application_id', // environments 테이블의 외래 키... 'environment_id', // deployments 테이블의 외래 키... 'id', // applications 테이블의 로컬 키... 'id' // environments 테이블의 로컬 키... ); } }

또는 앞서 설명한 것처럼, 관계에 관련된 모든 모델에 이미 필요한 관계가 정의되어 있다면, through 메서드를 호출하고 관계 이름들을 전달하는 방식으로 "has-many-through" 관계를 유연하게 정의할 수 있습니다. 이 방식은 기존 관계에 이미 정의된 키 관례를 그대로 재사용할 수 있다는 장점이 있습니다.

// 문자열 기반 문법... return $this->through('environments')->has('deployments'); // 동적 문법... return $this->throughEnvironments()->hasDeployments();

범위가 제한된 관계(Scoped Relationships)

모델에 관계를 제약하는 메서드를 추가하는 경우가 종종 있습니다. 예를 들어 User 모델에 featuredPosts 메서드를 추가해서, 더 넓은 범위의 posts 관계에 where 조건을 덧붙여 제한할 수 있습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; class User extends Model { /** * 이 사용자의 게시글 목록을 가져옵니다. */ public function posts(): HasMany { return $this->hasMany(Post::class)->latest(); } /** * 이 사용자의 추천 게시글 목록을 가져옵니다. */ public function featuredPosts(): HasMany { return $this->posts()->where('featured', true); } }

그런데 featuredPosts 메서드를 통해 모델을 새로 생성하면, featured 속성이 자동으로 true로 설정되지 않습니다. 관계 메서드를 통해 모델을 생성할 때, 해당 관계로 생성되는 모든 모델에 특정 속성 값을 자동으로 지정하고 싶다면, 관계 쿼리를 작성할 때 withAttributes 메서드를 사용하면 됩니다.

/** * 이 사용자의 추천 게시글 목록을 가져옵니다. */ public function featuredPosts(): HasMany { return $this->posts()->withAttributes(['featured' => true]); }

withAttributes 메서드는 전달받은 속성을 기준으로 쿼리에 where 조건을 추가할 뿐만 아니라, 해당 관계 메서드를 통해 생성되는 모델에도 동일한 속성 값을 자동으로 지정해줍니다.

$post = $user->featuredPosts()->create(['title' => 'Featured Post']); $post->featured; // true

withAttributes 메서드가 쿼리에 where 조건을 추가하지 않도록 하려면, asConditions 인자를 false로 설정하면 됩니다.

return $this->posts()->withAttributes(['featured' => true], asConditions: false);

다대다 관계 (Many to Many Relationships)

다대다 관계는 hasOne이나 hasMany 관계보다 조금 더 복잡한 형태입니다. 대표적인 예로 "여러 역할(Role)을 가진 사용자"를 들 수 있는데, 이때 각 역할은 다른 사용자들과도 공유됩니다. 예를 들어 어떤 사용자에게 "작성자(Author)"와 "편집자(Editor)" 역할이 부여될 수 있고, 동시에 이 역할들은 다른 사용자에게도 부여될 수 있습니다. 즉, 사용자는 여러 역할을 가질 수 있고, 역할 또한 여러 사용자를 가질 수 있는 관계입니다.

테이블 구조

이 관계를 정의하려면 users, roles, role_user 이렇게 세 개의 테이블이 필요합니다. role_user 테이블은 두 모델명을 알파벳 순서로 조합한 이름을 가지며, user_idrole_id 컬럼을 포함합니다. 이 테이블은 사용자와 역할을 연결해주는 중간 테이블(intermediate table) 역할을 합니다.

한 가지 역할이 여러 사용자에게 속할 수 있다는 점을 기억해야 합니다. 그렇기 때문에 roles 테이블에 단순히 user_id 컬럼을 추가하는 방식으로는 해결할 수 없습니다. 그렇게 하면 하나의 역할이 오직 한 명의 사용자에게만 속할 수 있게 되어버리기 때문입니다. 여러 사용자에게 역할을 할당할 수 있도록 지원하려면 role_user 중간 테이블이 필요합니다. 이 관계의 테이블 구조를 정리하면 다음과 같습니다.

users
    id - integer
    name - string

roles
    id - integer
    name - string

role_user
    user_id - integer
    role_id - integer

모델 구조

다대다 관계는 belongsToMany 메서드의 결과를 반환하는 메서드를 작성하여 정의합니다. belongsToMany 메서드는 모든 Eloquent 모델이 상속하는 Illuminate\Database\Eloquent\Model 기본 클래스에서 제공됩니다. 예를 들어, User 모델에 roles 메서드를 정의해봅시다. 이 메서드에 전달하는 첫 번째 인자는 관계를 맺을 모델의 클래스명입니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class User extends Model { /** * 사용자가 가진 역할들 */ public function roles(): BelongsToMany { return $this->belongsToMany(Role::class); } }

관계를 정의한 후에는 roles 동적 관계 속성을 통해 사용자의 역할 목록에 접근할 수 있습니다.

use App\Models\User; $user = User::find(1); foreach ($user->roles as $role) { // ... }

모든 관계는 쿼리 빌더 역할도 겸하기 때문에, roles 메서드를 호출한 뒤 조건을 계속 체이닝하여 관계 쿼리에 추가 제약을 걸 수 있습니다.

$roles = User::find(1)->roles()->orderBy('name')->get();

중간 테이블의 이름을 결정할 때 Eloquent는 관계를 맺는 두 모델명을 알파벳 순서로 이어붙입니다. 하지만 이 관례를 자유롭게 재정의할 수 있는데, belongsToMany 메서드의 두 번째 인자로 원하는 테이블명을 전달하면 됩니다.

return $this->belongsToMany(Role::class, 'role_user');

중간 테이블의 이름뿐만 아니라, belongsToMany 메서드에 추가 인자를 전달하여 테이블에 있는 키 컬럼명도 커스터마이징할 수 있습니다. 세 번째 인자는 관계를 정의하는 모델의 외래 키명이고, 네 번째 인자는 조인 대상 모델의 외래 키명입니다.

return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');

관계의 역방향 정의하기

다대다 관계의 "역방향"을 정의하려면, 관계를 맺는 대상 모델에도 belongsToMany 메서드의 결과를 반환하는 메서드를 정의해야 합니다. 앞서 살펴본 사용자/역할 예제를 완성하기 위해, Role 모델에 users 메서드를 정의해보겠습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Role extends Model { /** * 이 역할에 속한 사용자들 */ public function users(): BelongsToMany { return $this->belongsToMany(User::class); } }

보시다시피 App\Models\User 모델을 참조한다는 점을 제외하면 User 모델에서 정의한 것과 완전히 동일한 방식으로 관계를 정의합니다. 동일하게 belongsToMany 메서드를 재사용하므로, 다대다 관계의 "역방향"을 정의할 때도 테이블명이나 키 커스터마이징 옵션을 그대로 사용할 수 있습니다.

중간 테이블 컬럼 조회하기

앞서 살펴본 대로, 다대다 관계를 다루려면 중간 테이블의 존재가 필수적입니다. Eloquent는 이 테이블을 다루기 위한 매우 유용한 방법들을 제공합니다. 예를 들어 User 모델이 여러 Role 모델과 관계를 맺고 있다고 가정해봅시다. 관계에 접근한 후에는 모델의 pivot 속성을 통해 중간 테이블에 접근할 수 있습니다.

use App\Models\User; $user = User::find(1); foreach ($user->roles as $role) { echo $role->pivot->created_at; }

조회된 각각의 Role 모델에는 자동으로 pivot 속성이 부여된다는 점에 주목하세요. 이 속성은 중간 테이블을 나타내는 모델을 담고 있습니다.

기본적으로 pivot 모델에는 두 모델의 키 값만 존재합니다. 만약 중간 테이블에 추가 속성이 있다면, 관계를 정의할 때 이를 명시해야 합니다.

return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');

중간 테이블에 Eloquent가 자동으로 관리하는 created_at, updated_at 타임스탬프를 두고 싶다면, 관계를 정의할 때 withTimestamps 메서드를 호출하세요.

return $this->belongsToMany(Role::class)->withTimestamps();

WARNING

Eloquent가 자동으로 관리하는 타임스탬프를 사용하는 중간 테이블에는 created_atupdated_at 컬럼이 모두 존재해야 합니다.

`pivot` 속성명 커스터마이징하기

앞서 설명한 것처럼 중간 테이블의 속성은 모델의 pivot 속성을 통해 접근할 수 있습니다. 하지만 애플리케이션의 목적에 더 잘 맞도록 이 속성명을 자유롭게 변경할 수 있습니다.

예를 들어 애플리케이션에서 사용자가 팟캐스트를 구독하는 기능이 있다면, 사용자와 팟캐스트 사이에 다대다 관계가 존재할 것입니다. 이런 경우 중간 테이블 속성명을 pivot 대신 subscription으로 바꾸고 싶을 수 있습니다. 이는 관계를 정의할 때 as 메서드를 사용하면 됩니다.

return $this->belongsToMany(Podcast::class) ->as('subscription') ->withTimestamps();

커스텀 중간 테이블 속성명을 지정한 후에는, 변경한 이름으로 중간 테이블 데이터에 접근할 수 있습니다.

$users = User::with('podcasts')->get(); foreach ($users->flatMap->podcasts as $podcast) { echo $podcast->subscription->created_at; }

중간 테이블 컬럼으로 쿼리 필터링하기

관계를 정의할 때 wherePivot, wherePivotIn, wherePivotNotIn, wherePivotBetween, wherePivotNotBetween, wherePivotNull, wherePivotNotNull 메서드를 사용하면 belongsToMany 관계 쿼리가 반환하는 결과를 필터링할 수 있습니다.

return $this->belongsToMany(Role::class) ->wherePivot('approved', 1); return $this->belongsToMany(Role::class) ->wherePivotIn('priority', [1, 2]); return $this->belongsToMany(Role::class) ->wherePivotNotIn('priority', [1, 2]); return $this->belongsToMany(Podcast::class) ->as('subscriptions') ->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']); return $this->belongsToMany(Podcast::class) ->as('subscriptions') ->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']); return $this->belongsToMany(Podcast::class) ->as('subscriptions') ->wherePivotNull('expired_at'); return $this->belongsToMany(Podcast::class) ->as('subscriptions') ->wherePivotNotNull('expired_at');

wherePivot은 쿼리에 where 절 조건을 추가하지만, 정의된 관계를 통해 새 모델을 생성할 때는 지정한 값을 자동으로 채워주지 않습니다. 특정 pivot 값으로 관계를 조회하면서 동시에 생성도 하고 싶다면 withPivotValue 메서드를 사용하세요.

return $this->belongsToMany(Role::class) ->withPivotValue('approved', 1);

중간 테이블 컬럼으로 쿼리 정렬하기

orderByPivotorderByPivotDesc 메서드를 사용하면 belongsToMany 관계 쿼리가 반환하는 결과를 정렬할 수 있습니다. 다음 예제는 사용자가 획득한 뱃지 중 최신 항목을 조회합니다.

return $this->belongsToMany(Badge::class) ->where('rank', 'gold') ->orderByPivotDesc('created_at');

커스텀 중간 테이블 모델 정의하기

다대다 관계의 중간 테이블을 나타내는 커스텀 모델을 정의하고 싶다면, 관계를 정의할 때 using 메서드를 호출하면 됩니다. 커스텀 pivot 모델을 사용하면 메서드나 캐스트(cast) 같은 추가 동작을 pivot 모델에 정의할 수 있는 장점이 있습니다.

커스텀 다대다 pivot 모델은 Illuminate\Database\Eloquent\Relations\Pivot 클래스를 상속해야 하며, 커스텀 폴리모픽 다대다 pivot 모델은 Illuminate\Database\Eloquent\Relations\MorphPivot 클래스를 상속해야 합니다. 예를 들어, 커스텀 RoleUser pivot 모델을 사용하는 Role 모델을 다음과 같이 정의할 수 있습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Role extends Model { /** * 이 역할에 속한 사용자들 */ public function users(): BelongsToMany { return $this->belongsToMany(User::class)->using(RoleUser::class); } }

RoleUser 모델을 정의할 때는 Illuminate\Database\Eloquent\Relations\Pivot 클래스를 상속해야 합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Relations\Pivot; class RoleUser extends Pivot { // ... }

WARNING

Pivot 모델은 SoftDeletes 트레이트를 사용할 수 없습니다. pivot 레코드를 소프트 삭제해야 한다면, pivot 모델을 실제 Eloquent 모델로 전환하는 것을 고려해보세요.

커스텀 Pivot 모델과 자동 증가 ID

커스텀 pivot 모델을 사용하는 다대다 관계를 정의했고, 그 pivot 모델이 자동 증가하는 기본 키를 가지고 있다면, 커스텀 pivot 모델 클래스에 incrementingtrue로 설정한 Table 어트리뷰트를 반드시 사용해야 합니다.

use Illuminate\Database\Eloquent\Attributes\Table; use Illuminate\Database\Eloquent\Relations\Pivot; #[Table(incrementing: true)] class RoleUser extends Pivot { // ... }

Pivot 관계 자동 하이드레이션

커스텀 pivot 모델이 관계를 선언한 모델과 대상 모델에 대해 각각 belongsTo 관계를 정의하고 있다면, chaperone 메서드를 호출하여 각 pivot 모델에서 이 관계들을 자동으로 하이드레이션할 수 있습니다. 이렇게 하면 pivot을 통해 모델에 접근할 때 발생하는 추가 쿼리를 피할 수 있습니다.

use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Pivot; class RoleUser extends Pivot { public function role(): BelongsTo { return $this->belongsTo(Role::class); } public function user(): BelongsTo { return $this->belongsTo(User::class); } } class Role extends Model { public function users(): BelongsToMany { return $this->belongsToMany(User::class) ->using(RoleUser::class) ->chaperone(); } }

Eloquent는 pivot 관계의 이름을 자동으로 추론합니다. pivot 모델이 표준적이지 않은 이름을 사용한다면, chaperone에 관계를 선언한 쪽과 대상 쪽의 관계 이름을 직접 전달하세요.

return $this->belongsToMany(User::class) ->using(RoleUser::class) ->chaperone(declaring: 'role', related: 'user');

Eloquent 관계

다형적 관계 (Polymorphic Relationships)

다형적 관계(polymorphic relationship)를 사용하면 자식 모델이 단 하나의 연관 관계만으로 여러 종류의 모델에 속할 수 있습니다. 예를 들어, 사용자가 게시글(blog post)과 동영상을 공유할 수 있는 애플리케이션을 만든다고 가정해 보겠습니다. 이런 경우 Comment 모델은 Post 모델과 Video 모델 양쪽에 모두 속할 수 있어야 합니다.

일반적인 관계(예: hasMany, belongsTo)는 자식 모델이 오직 하나의 부모 모델 종류에만 속할 수 있다는 전제를 갖습니다. 다형적 관계는 이 제약을 없애고, "이 댓글의 부모가 게시글인지 동영상인지"를 데이터베이스 컬럼으로 함께 저장해서 해결합니다.

1:1 다형적 관계 (One to One)

테이블 구조

1:1 다형적 관계는 일반적인 1:1 관계와 비슷하지만, 자식 모델이 단 하나의 연관 관계로 여러 종류의 모델에 속할 수 있다는 점이 다릅니다. 예를 들어 PostUserImage 모델과 다형적 관계를 공유한다고 해봅시다. 1:1 다형적 관계를 사용하면 게시글과 사용자 모두에 연결될 수 있는, 중복 없는 하나의 이미지 테이블을 만들 수 있습니다. 먼저 테이블 구조를 살펴보겠습니다.

posts
    id - integer
    name - string

users
    id - integer
    name - string

images
    id - integer
    url - string
    imageable_type - string
    imageable_id - integer

images 테이블의 imageable_id, imageable_type 컬럼에 주목하세요. imageable_id 컬럼에는 게시글이나 사용자의 ID 값이 저장되고, imageable_type 컬럼에는 부모 모델의 클래스명이 저장됩니다. Eloquent는 imageable 관계에 접근할 때 이 imageable_type 컬럼 값을 보고 어떤 "타입"의 부모 모델을 반환해야 할지 판단합니다. 이 예제에서는 App\Models\Post 또는 App\Models\User 값이 저장됩니다.

모델 구조

다음으로 이 관계를 구성하는 데 필요한 모델 정의를 살펴보겠습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; class Image extends Model { /** * 이 이미지를 소유한 부모 모델(user 또는 post)을 가져옵니다. */ public function imageable(): MorphTo { return $this->morphTo(); } } use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphOne; class Post extends Model { /** * 게시글의 이미지를 가져옵니다. */ public function image(): MorphOne { return $this->morphOne(Image::class, 'imageable'); } } use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphOne; class User extends Model { /** * 사용자의 이미지를 가져옵니다. */ public function image(): MorphOne { return $this->morphOne(Image::class, 'imageable'); } }

관계 조회하기

데이터베이스 테이블과 모델이 정의됐다면, 이제 모델을 통해 관계에 접근할 수 있습니다. 예를 들어 게시글의 이미지를 조회하려면 image 동적 관계 프로퍼티를 사용하면 됩니다.

use App\Models\Post; $post = Post::find(1); $image = $post->image;

다형적 모델의 부모는 morphTo를 호출하는 메서드명을 통해 접근할 수 있습니다. 이 예제에서는 Image 모델의 imageable 메서드가 그 역할을 합니다. 따라서 다음과 같이 동적 관계 프로퍼티로 접근합니다.

use App\Models\Image; $image = Image::find(1); $imageable = $image->imageable;

Image 모델의 imageable 관계는 해당 이미지를 소유한 모델이 무엇이냐에 따라 Post 또는 User 인스턴스를 반환합니다.

키 규칙(Key Conventions)

필요하다면 다형적 자식 모델에서 사용하는 "id" 컬럼과 "type" 컬럼의 이름을 직접 지정할 수 있습니다. 이때는 반드시 morphTo 메서드의 첫 번째 인자로 관계 이름을 전달해야 합니다. 이 값은 보통 메서드 이름과 동일하게 맞추므로, PHP의 __FUNCTION__ 상수를 활용할 수 있습니다.

/** * 이 이미지가 속한 모델을 가져옵니다. */ public function imageable(): MorphTo { return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id'); }

1:N 다형적 관계 (One to Many)

테이블 구조

1:N 다형적 관계는 일반적인 1:N 관계와 비슷하지만, 자식 모델이 단 하나의 연관 관계로 여러 종류의 모델에 속할 수 있다는 점이 다릅니다. 예를 들어 애플리케이션 사용자가 게시글과 동영상에 "댓글"을 남길 수 있다고 가정해 보겠습니다. 다형적 관계를 활용하면 게시글과 동영상의 댓글을 하나의 comments 테이블로 모두 처리할 수 있습니다. 먼저 이 관계를 구성하는 데 필요한 테이블 구조를 살펴보겠습니다.

posts
    id - integer
    title - string
    body - text

videos
    id - integer
    title - string
    url - string

comments
    id - integer
    body - text
    commentable_type - string
    commentable_id - integer

모델 구조

다음으로 이 관계를 구성하는 데 필요한 모델 정의를 살펴보겠습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; class Comment extends Model { /** * 이 댓글의 부모 모델(post 또는 video)을 가져옵니다. */ public function commentable(): MorphTo { return $this->morphTo(); } } use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphMany; class Post extends Model { /** * 게시글의 모든 댓글을 가져옵니다. */ public function comments(): MorphMany { return $this->morphMany(Comment::class, 'commentable'); } } use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphMany; class Video extends Model { /** * 동영상의 모든 댓글을 가져옵니다. */ public function comments(): MorphMany { return $this->morphMany(Comment::class, 'commentable'); } }

관계 조회하기

데이터베이스 테이블과 모델이 정의됐다면, 모델의 동적 관계 프로퍼티를 통해 관계에 접근할 수 있습니다. 예를 들어 게시글의 모든 댓글을 조회하려면 comments 동적 프로퍼티를 사용합니다.

use App\Models\Post; $post = Post::find(1); foreach ($post->comments as $comment) { // ... }

또한 morphTo를 호출하는 메서드명을 통해 다형적 자식 모델의 부모를 조회할 수도 있습니다. 이 예제에서는 Comment 모델의 commentable 메서드가 그 역할을 합니다. 댓글의 부모 모델에 접근하려면 다음과 같이 동적 관계 프로퍼티로 접근하면 됩니다.

use App\Models\Comment; $comment = Comment::find(1); $commentable = $comment->commentable;

Comment 모델의 commentable 관계는 해당 댓글의 부모가 무엇이냐에 따라 Post 또는 Video 인스턴스를 반환합니다.

자식 모델에 부모 모델 자동으로 채우기

Eloquent의 즉시 로딩(eager loading)을 사용하더라도, 자식 모델을 순회하면서 그 안에서 다시 부모 모델에 접근하려고 하면 "N+1" 쿼리 문제가 발생할 수 있습니다.

$posts = Post::with('comments')->get(); foreach ($posts as $post) { foreach ($post->comments as $comment) { echo $comment->commentable->title; } }

위 예제에서는 "N+1" 쿼리 문제가 발생합니다. 각 Post 모델에 대해 댓글은 즉시 로딩되었지만, Eloquent가 각 Comment 자식 모델에 부모 Post를 자동으로 채워 넣지는 않기 때문입니다.

Eloquent가 자식 모델에 부모 모델을 자동으로 채우도록 하려면, morphMany 관계를 정의할 때 chaperone 메서드를 호출하면 됩니다.

class Post extends Model { /** * 게시글의 모든 댓글을 가져옵니다. */ public function comments(): MorphMany { return $this->morphMany(Comment::class, 'commentable')->chaperone(); } }

또는 실행 시점(runtime)에만 부모 모델 자동 채우기를 적용하고 싶다면, 관계를 즉시 로딩할 때 chaperone 메서드를 호출할 수 있습니다.

use App\Models\Post; $posts = Post::with([ 'comments' => fn ($comments) => $comments->chaperone(), ])->get();

다형적 최신/최초 1건 관계 (One of Many)

때로는 모델이 여러 개의 연관 모델을 가지고 있지만, 그중 "가장 최신" 또는 "가장 오래된" 연관 모델 하나만 손쉽게 조회하고 싶을 때가 있습니다. 예를 들어 User 모델이 여러 Image 모델과 연결되어 있는데, 사용자가 가장 최근에 업로드한 이미지에 편리하게 접근하고 싶은 경우입니다. 이럴 때는 morphOne 관계와 ofMany 계열 메서드를 조합해서 사용할 수 있습니다.

/** * 사용자가 가장 최근에 업로드한 이미지를 가져옵니다. */ public function latestImage(): MorphOne { return $this->morphOne(Image::class, 'imageable')->latestOfMany(); }

마찬가지로 "가장 오래된", 즉 가장 처음의 연관 모델을 조회하는 메서드도 정의할 수 있습니다.

/** * 사용자가 가장 먼저 업로드한 이미지를 가져옵니다. */ public function oldestImage(): MorphOne { return $this->morphOne(Image::class, 'imageable')->oldestOfMany(); }

기본적으로 latestOfManyoldestOfMany는 모델의 기본 키(정렬 가능해야 함)를 기준으로 최신 또는 최초의 연관 모델을 조회합니다. 하지만 더 큰 관계에서 다른 정렬 기준으로 단일 모델을 조회하고 싶을 때도 있을 것입니다.

예를 들어 ofMany 메서드를 사용하면 사용자가 가장 많은 "좋아요"를 받은 이미지를 조회할 수 있습니다. ofMany는 첫 번째 인자로 정렬 기준이 되는 컬럼을, 두 번째 인자로 연관 모델을 조회할 때 적용할 집계 함수(min 또는 max)를 받습니다.

/** * 사용자의 가장 인기 있는 이미지를 가져옵니다. */ public function bestImage(): MorphOne { return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max'); }

NOTE

이보다 더 복잡한 "최신/최초 1건" 관계를 구성하는 것도 가능합니다. 자세한 내용은 has one of many 문서를 참고하세요.

N:M 다형적 관계 (Many to Many)

테이블 구조

N:M 다형적 관계는 "morph one"이나 "morph many" 관계보다 조금 더 복잡합니다. 예를 들어 Post 모델과 Video 모델이 Tag 모델과 다형적 관계를 공유한다고 해봅시다. 이런 상황에서 N:M 다형적 관계를 사용하면, 게시글이나 동영상에 자유롭게 연결할 수 있는 중복 없는 하나의 태그 테이블을 만들 수 있습니다. 먼저 이 관계를 구성하는 데 필요한 테이블 구조를 살펴보겠습니다.

posts
    id - integer
    name - string

videos
    id - integer
    name - string

tags
    id - integer
    name - string

taggables
    tag_id - integer
    taggable_type - string
    taggable_id - integer

NOTE

다형적 N:M 관계를 살펴보기 전에, 먼저 일반적인 N:M 관계 문서를 읽어보면 이해에 도움이 됩니다.

모델 구조

이제 모델에 관계를 정의할 차례입니다. PostVideo 모델 모두 Eloquent 기본 모델 클래스가 제공하는 morphToMany 메서드를 호출하는 tags 메서드를 가지게 됩니다.

morphToMany 메서드는 연관 모델명과 "관계 이름(relationship name)"을 인자로 받습니다. 여기서는 중간 테이블 이름과 그 안의 키를 기준으로 관계 이름을 "taggable"로 지정하겠습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphToMany; class Post extends Model { /** * 게시글의 모든 태그를 가져옵니다. */ public function tags(): MorphToMany { return $this->morphToMany(Tag::class, 'taggable'); } }

역방향 관계 정의하기

다음으로 Tag 모델에는 가능한 각 부모 모델에 대응하는 메서드를 정의해야 합니다. 이 예제에서는 posts 메서드와 videos 메서드를 정의하겠습니다. 두 메서드 모두 morphedByMany 메서드의 결과를 반환해야 합니다.

morphedByMany 메서드는 연관 모델명과 "관계 이름"을 인자로 받습니다. 여기서도 중간 테이블 이름과 그 안의 키를 기준으로 관계 이름을 "taggable"로 지정합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphToMany; class Tag extends Model { /** * 이 태그가 지정된 모든 게시글을 가져옵니다. */ public function posts(): MorphToMany { return $this->morphedByMany(Post::class, 'taggable'); } /** * 이 태그가 지정된 모든 동영상을 가져옵니다. */ public function videos(): MorphToMany { return $this->morphedByMany(Video::class, 'taggable'); } }

관계 조회하기

데이터베이스 테이블과 모델이 정의됐다면, 이제 모델을 통해 관계에 접근할 수 있습니다. 예를 들어 게시글의 모든 태그에 접근하려면 tags 동적 관계 프로퍼티를 사용하면 됩니다.

use App\Models\Post; $post = Post::find(1); foreach ($post->tags as $tag) { // ... }

다형적 자식 모델에서 부모를 조회할 때는 morphedByMany를 호출하는 메서드명을 통해 접근합니다. 이 예제에서는 Tag 모델의 posts, videos 메서드가 그 역할을 합니다.

use App\Models\Tag; $tag = Tag::find(1); foreach ($tag->posts as $post) { // ... } foreach ($tag->videos as $video) { // ... }

다형적 타입 커스터마이징

기본적으로 Laravel은 연관 모델의 "타입"을 저장할 때 완전히 정규화된 클래스명(fully qualified class name)을 사용합니다. 예를 들어 앞서 살펴본 1:N 관계 예제에서 Comment 모델이 PostVideo 모델에 속할 수 있는 경우, 기본적으로 commentable_type 값은 각각 App\Models\Post, App\Models\Video가 됩니다. 하지만 이런 값들을 애플리케이션의 내부 구조와 분리하고 싶을 수도 있습니다.

예를 들어 모델명을 그대로 "타입"으로 사용하는 대신 post, video처럼 단순한 문자열을 사용할 수 있습니다. 이렇게 하면 나중에 모델의 이름이 바뀌더라도 데이터베이스에 저장된 다형적 "타입" 컬럼 값은 계속 유효하게 유지됩니다.

use Illuminate\Database\Eloquent\Relations\Relation; Relation::enforceMorphMap([ 'post' => 'App\Models\Post', 'video' => 'App\Models\Video', ]);

enforceMorphMap 메서드는 App\Providers\AppServiceProvider 클래스의 boot 메서드 안에서 호출하거나, 원한다면 별도의 서비스 프로바이더를 만들어 그 안에서 호출해도 됩니다.

특정 모델의 morph 별칭(alias)은 모델의 getMorphClass 메서드로 런타임에 확인할 수 있습니다. 반대로 morph 별칭에 대응하는 완전히 정규화된 클래스명은 Relation::getMorphedModel 메서드로 확인할 수 있습니다.

use Illuminate\Database\Eloquent\Relations\Relation; $alias = $post->getMorphClass(); $class = Relation::getMorphedModel($alias);

WARNING

이미 운영 중인 애플리케이션에 "morph map"을 새로 도입하는 경우, 데이터베이스에 저장된 모든 다형적 *_type 컬럼 값 중 완전히 정규화된 클래스명이 남아 있는 것들은 모두 map에서 정의한 이름으로 변환해줘야 합니다.

동적 관계 정의

resolveRelationUsing 메서드를 사용하면 런타임에 Eloquent 모델 간의 관계를 정의할 수 있습니다. 일반적인 애플리케이션 개발에서는 잘 사용하지 않지만, Laravel 패키지를 개발할 때는 종종 유용하게 쓰일 수 있습니다.

resolveRelationUsing 메서드의 첫 번째 인자는 정의하고자 하는 관계의 이름입니다. 두 번째 인자는 모델 인스턴스를 받아서 유효한 Eloquent 관계 정의를 반환하는 클로저입니다. 일반적으로 동적 관계는 서비스 프로바이더의 boot 메서드 안에서 설정해야 합니다.

use App\Models\Order; use App\Models\Customer; Order::resolveRelationUsing('customer', function (Order $orderModel) { return $orderModel->belongsTo(Customer::class, 'customer_id'); });

WARNING

동적 관계를 정의할 때는 Eloquent 관계 메서드에 키 이름 인자를 항상 명시적으로 전달해야 합니다.

관련 모델 조회하기

Querying Relations (관계 쿼리하기)

Eloquent 관계는 모두 메서드로 정의되기 때문에, 실제로 관련 모델을 로드하는 쿼리를 실행하지 않고도 해당 메서드를 호출해서 관계 인스턴스를 가져올 수 있습니다. 게다가 모든 Eloquent 관계 타입은 그 자체로 쿼리 빌더 역할도 하기 때문에, 데이터베이스에 최종적으로 SQL 쿼리를 실행하기 전에 관계 쿼리에 계속해서 조건을 체이닝할 수 있습니다.

예를 들어, User 모델이 여러 개의 Post 모델을 가지는 블로그 애플리케이션을 생각해봅시다:

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; class User extends Model { /** * 해당 사용자의 모든 게시글을 가져옵니다. */ public function posts(): HasMany { return $this->hasMany(Post::class); } }

다음과 같이 posts 관계를 쿼리하면서 추가 조건을 붙일 수 있습니다:

use App\Models\User; $user = User::find(1); $user->posts()->where('active', 1)->get();

관계에 대해서도 Laravel 쿼리 빌더의 모든 메서드를 사용할 수 있으므로, 쿼리 빌더 문서를 살펴보고 어떤 메서드들을 사용할 수 있는지 확인해보시길 권장합니다.

관계 뒤에 `orWhere` 조건절 체이닝하기

위 예제처럼 관계를 쿼리할 때 추가 조건을 자유롭게 붙일 수 있습니다. 다만 관계에 orWhere 조건절을 체이닝할 때는 주의가 필요합니다. orWhere 조건절은 관계 자체의 제약 조건과 같은 레벨로 논리적으로 그룹화되기 때문입니다:

$user->posts() ->where('active', 1) ->orWhere('votes', '>=', 100) ->get();

위 예제는 다음과 같은 SQL을 생성합니다. 보시다시피 or 조건절 때문에 특정 사용자에 한정되지 않고 투표 수가 100 이상인 모든 게시글을 반환하게 됩니다. 즉, 쿼리가 더 이상 특정 사용자로 제한되지 않습니다:

select * from posts where user_id = ? and active = 1 or votes >= 100

대부분의 경우, 조건 검사를 괄호로 묶어 논리적 그룹화를 사용해야 합니다:

use Illuminate\Database\Eloquent\Builder; $user->posts() ->where(function (Builder $query) { return $query->where('active', 1) ->orWhere('votes', '>=', 100); }) ->get();

위 예제는 다음과 같은 SQL을 생성합니다. 이번에는 논리적 그룹화가 제대로 적용되어 쿼리가 특정 사용자로 제한된 상태를 유지하는 것을 확인할 수 있습니다:

select * from posts where user_id = ? and (active = 1 or votes >= 100)

관계 메서드 방식 vs. 동적 프로퍼티 방식

Eloquent 관계 쿼리에 추가 조건을 붙일 필요가 없다면, 관계를 마치 프로퍼티처럼 접근해서 사용할 수도 있습니다. 예를 들어 앞서 사용한 UserPost 모델 예제를 계속 사용한다면, 다음과 같이 사용자의 모든 게시글에 접근할 수 있습니다:

use App\Models\User; $user = User::find(1); foreach ($user->posts as $post) { // ... }

동적 관계 프로퍼티는 "지연 로딩(lazy loading)"으로 동작합니다. 즉, 실제로 해당 프로퍼티에 접근하는 순간에만 관계 데이터를 로드합니다. 이런 특성 때문에, 개발자들은 나중에 사용할 것이 확실한 관계를 미리 로드해두는 즉시 로딩(eager loading)을 자주 활용합니다. 즉시 로딩을 사용하면 모델의 관계를 로드하기 위해 실행해야 하는 SQL 쿼리 수를 크게 줄일 수 있습니다.

NOTE

지연 로딩과 즉시 로딩을 혼동하기 쉬운데, 간단히 구분하면 지연 로딩은 "필요할 때 그때그때 하나씩" 쿼리를 실행하는 방식(이른바 N+1 문제의 원인)이고, 즉시 로딩은 "미리 한꺼번에" 쿼리를 실행해두는 방식입니다. 게시글 목록을 순회하면서 각 게시글의 작성자 정보를 매번 조회한다면 지연 로딩으로 인해 N+1 쿼리가 발생할 수 있으니 주의하세요.

모델 레코드를 조회할 때, 관계의 존재 여부를 기준으로 결과를 제한하고 싶을 때가 있습니다. 예를 들어 댓글이 하나 이상 달린 블로그 게시글만 조회하고 싶다고 가정해봅시다. 이럴 때는 hasorHas 메서드에 관계 이름을 전달하면 됩니다:

use App\Models\Post; // 댓글이 하나 이상 있는 모든 게시글을 조회합니다... $posts = Post::has('comments')->get();

연산자와 개수 조건을 지정해서 쿼리를 더 세밀하게 조정할 수도 있습니다:

// 댓글이 세 개 이상인 모든 게시글을 조회합니다... $posts = Post::has('comments', '>=', 3)->get();

has 구문은 "점(.) 표기법"을 사용해서 중첩된 형태로 작성할 수도 있습니다. 예를 들어 이미지가 첨부된 댓글이 하나 이상 있는 게시글을 조회하려면 다음과 같이 작성합니다:

// 이미지가 첨부된 댓글이 하나 이상 있는 게시글을 조회합니다... $posts = Post::has('comments.images')->get();

더 강력한 기능이 필요하다면, whereHasorWhereHas 메서드를 사용해서 has 쿼리에 추가적인 제약 조건을 정의할 수 있습니다. 예를 들어 댓글의 내용을 검사하는 조건을 추가할 수 있습니다:

use Illuminate\Database\Eloquent\Builder; // code%로 시작하는 단어가 포함된 댓글이 하나 이상 있는 게시글을 조회합니다... $posts = Post::whereHas('comments', function (Builder $query) { $query->where('content', 'like', 'code%'); })->get(); // code%로 시작하는 단어가 포함된 댓글이 열 개 이상 있는 게시글을 조회합니다... $posts = Post::whereHas('comments', function (Builder $query) { $query->where('content', 'like', 'code%'); }, '>=', 10)->get();

WARNING

Eloquent는 현재 여러 데이터베이스에 걸친 관계 존재 여부 쿼리를 지원하지 않습니다. 관계를 구성하는 모델들은 반드시 동일한 데이터베이스 내에 존재해야 합니다.

whereAttachedTo 메서드를 사용하면 특정 모델 또는 모델 컬렉션에 다대다 관계로 연결(attach)되어 있는 모델을 조회할 수 있습니다:

$users = User::whereAttachedTo($role)->get();

whereAttachedTo 메서드에는 컬렉션 인스턴스를 전달할 수도 있습니다. 이 경우 Laravel은 컬렉션에 담긴 모델들 중 어느 하나에라도 연결된 모델들을 조회합니다:

$tags = Tag::whereLike('name', '%laravel%')->get(); $posts = Post::whereAttachedTo($tags)->get();

관계 쿼리에 단순한 where 조건 하나만 붙여서 관계 존재 여부를 확인하고 싶다면, whereRelation, orWhereRelation, whereMorphRelation, orWhereMorphRelation 메서드를 사용하는 것이 더 편리할 수 있습니다. 예를 들어 승인되지 않은 댓글이 있는 모든 게시글을 조회해봅시다:

use App\Models\Post; $posts = Post::whereRelation('comments', 'is_approved', false)->get();

물론 쿼리 빌더의 where 메서드를 호출할 때처럼 연산자를 지정할 수도 있습니다:

$posts = Post::whereRelation( 'comments', 'created_at', '>=', now()->minus(hours: 1) )->get();

관계 부재 여부로 쿼리하기

모델 레코드를 조회할 때, 반대로 관계가 존재하지 않는 것을 기준으로 결과를 제한하고 싶을 때도 있습니다. 예를 들어 댓글이 하나도 없는 블로그 게시글만 조회하고 싶다면, doesntHaveorDoesntHave 메서드에 관계 이름을 전달하면 됩니다:

use App\Models\Post; $posts = Post::doesntHave('comments')->get();

더 강력한 기능이 필요하다면, whereDoesntHaveorWhereDoesntHave 메서드를 사용해서 doesntHave 쿼리에 추가 제약 조건을 붙일 수 있습니다. 예를 들어 댓글 내용을 검사하는 조건을 추가할 수 있습니다:

use Illuminate\Database\Eloquent\Builder; $posts = Post::whereDoesntHave('comments', function (Builder $query) { $query->where('content', 'like', 'code%'); })->get();

"점(.) 표기법"을 사용하면 중첩된 관계에 대해서도 쿼리를 실행할 수 있습니다. 예를 들어 다음 쿼리는 댓글이 아예 없는 게시글과, 댓글이 있더라도 그 댓글 작성자 중 차단된 사용자가 아무도 없는 게시글을 함께 조회합니다:

use Illuminate\Database\Eloquent\Builder; $posts = Post::whereDoesntHave('comments.author', function (Builder $query) { $query->where('banned', 1); })->get();

Morph To 관계 쿼리하기

"morph to" 관계의 존재 여부를 쿼리하려면 whereHasMorphwhereDoesntHaveMorph 메서드를 사용할 수 있습니다. 이 메서드들은 첫 번째 인수로 관계 이름을 받습니다. 두 번째 인수로는 쿼리에 포함할 관련 모델들의 이름을 전달합니다. 마지막으로 클로저를 전달해서 관계 쿼리를 원하는 대로 커스터마이징할 수 있습니다:

use App\Models\Comment; use App\Models\Post; use App\Models\Video; use Illuminate\Database\Eloquent\Builder; // 제목에 code%가 포함된 게시글 또는 영상에 달린 댓글을 조회합니다... $comments = Comment::whereHasMorph( 'commentable', [Post::class, Video::class], function (Builder $query) { $query->where('title', 'like', 'code%'); } )->get(); // 제목에 code%가 포함되지 않은 게시글에 달린 댓글을 조회합니다... $comments = Comment::whereDoesntHaveMorph( 'commentable', Post::class, function (Builder $query) { $query->where('title', 'like', 'code%'); } )->get();

때로는 관련된 폴리모픽 모델의 "타입"을 기준으로 쿼리 조건을 추가해야 할 수도 있습니다. whereHasMorph 메서드에 전달하는 클로저는 두 번째 인수로 $type 값을 받을 수 있는데, 이 인수를 통해 현재 만들어지고 있는 쿼리의 "타입"을 확인할 수 있습니다:

use Illuminate\Database\Eloquent\Builder; $comments = Comment::whereHasMorph( 'commentable', [Post::class, Video::class], function (Builder $query, string $type) { $column = $type === Post::class ? 'content' : 'title'; $query->where($column, 'like', 'code%'); } )->get();

가끔은 "morph to" 관계의 부모 모델의 자식들을 쿼리하고 싶을 때가 있습니다. 이럴 때는 whereMorphedTowhereNotMorphedTo 메서드를 사용하면 되는데, 이 메서드들은 전달된 모델에 맞는 적절한 morph 타입 매핑을 자동으로 알아냅니다. 첫 번째 인수로 morphTo 관계의 이름을, 두 번째 인수로 관련된 부모 모델을 전달합니다:

$comments = Comment::whereMorphedTo('commentable', $post) ->orWhereMorphedTo('commentable', $video) ->get();

모든 관련 모델 조회하기

가능한 폴리모픽 모델들의 배열을 일일이 전달하는 대신, 와일드카드 값으로 *를 전달할 수도 있습니다. 이렇게 하면 Laravel이 데이터베이스에서 가능한 모든 폴리모픽 타입을 조회하도록 지시합니다. 이 작업을 수행하기 위해 Laravel은 추가 쿼리를 한 번 더 실행합니다:

use Illuminate\Database\Eloquent\Builder; $comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) { $query->where('title', 'like', 'foo%'); })->get();

관계 모델 집계하기

관계 모델 개수 세기

연관된 모델을 실제로 로드하지 않고 개수만 필요한 경우가 있습니다. 이럴 때는 모델을 통째로 불러오는 대신 withCount 메서드를 사용하면 됩니다. withCount를 사용하면 결과로 반환되는 모델에 {relation}_count 형태의 속성이 추가됩니다:

use App\Models\Post; $posts = Post::withCount('comments')->get(); foreach ($posts as $post) { echo $post->comments_count; }

withCount 메서드에 배열을 전달하면 여러 관계의 개수를 한 번에 조회할 수 있고, 각 쿼리에 추가 조건을 걸 수도 있습니다:

use Illuminate\Database\Eloquent\Builder; $posts = Post::withCount(['votes', 'comments' => function (Builder $query) { $query->where('content', 'like', 'code%'); }])->get(); echo $posts[0]->votes_count; echo $posts[0]->comments_count;

또한 관계 개수 결과에 별칭(alias)을 지정할 수도 있습니다. 이를 활용하면 같은 관계에 대해 서로 다른 조건의 개수를 동시에 구할 수 있습니다:

use Illuminate\Database\Eloquent\Builder; $posts = Post::withCount([ 'comments', 'comments as pending_comments_count' => function (Builder $query) { $query->where('approved', false); }, ])->get(); echo $posts[0]->comments_count; echo $posts[0]->pending_comments_count;

나중에 개수 로드하기 (지연 카운트 로딩)

이미 조회를 마친 부모 모델에 대해 나중에 관계 개수를 추가로 로드해야 할 때는 loadCount 메서드를 사용합니다:

$book = Book::first(); $book->loadCount('genres');

개수를 계산하는 쿼리에 추가 조건을 걸고 싶다면, 개수를 세고자 하는 관계명을 키로 하는 배열을 전달하면 됩니다. 배열 값은 쿼리 빌더 인스턴스를 인자로 받는 클로저여야 합니다:

$book->loadCount(['reviews' => function (Builder $query) { $query->where('rating', 5); }]);

관계 개수 세기와 커스텀 select 문 함께 사용하기

withCountselect 문과 함께 사용할 경우, 반드시 select 메서드 다음에 withCount를 호출해야 합니다:

$posts = Post::select(['title', 'body']) ->withCount('comments') ->get();

NOTE

순서가 바뀌면 selectwithCount가 추가한 서브쿼리 컬럼을 덮어써버릴 수 있으므로 주의가 필요합니다.

그 밖의 집계 함수

withCount 외에도 Eloquent는 withMin, withMax, withAvg, withSum, withExists 메서드를 제공합니다. 이 메서드들은 결과 모델에 {relation}_{function}_{column} 형태의 속성을 추가합니다:

use App\Models\Post; $posts = Post::withSum('comments', 'votes')->get(); foreach ($posts as $post) { echo $post->comments_sum_votes; }

집계 함수의 결과를 다른 이름으로 접근하고 싶다면 별칭을 직접 지정할 수 있습니다:

$posts = Post::withSum('comments as total_comments', 'votes')->get(); foreach ($posts as $post) { echo $post->total_comments; }

loadCount와 마찬가지로, 위 집계 메서드들도 이미 조회된 Eloquent 모델에 대해 나중에 적용할 수 있는 지연 로딩 버전이 존재합니다:

$post = Post::first(); $post->loadSum('comments', 'votes');

이러한 집계 메서드를 select 문과 함께 사용할 때도 마찬가지로, 반드시 select 메서드 다음에 집계 메서드를 호출해야 합니다:

$posts = Post::select(['title', 'body']) ->withExists('comments') ->get();

Morph To 관계에서 관련 모델 개수 세기

"morph to" 관계를 즉시 로딩(eager load)하면서, 동시에 그 관계가 반환할 수 있는 다양한 엔티티별로 관련 모델의 개수까지 함께 조회하고 싶을 수 있습니다. 이런 경우 with 메서드와 morphTo 관계의 morphWithCount 메서드를 조합해서 사용할 수 있습니다.

예를 들어, Photo 모델과 Post 모델이 각각 ActivityFeed 모델을 생성할 수 있는 상황을 가정해봅시다. ActivityFeed 모델은 parentable이라는 이름의 "morph to" 관계를 정의하고 있어서, 특정 ActivityFeed 인스턴스에 대한 부모 Photo 또는 Post 모델을 조회할 수 있습니다. 여기에 추가로 Photo 모델은 Tag 모델과 "has many" 관계, Post 모델은 Comment 모델과 "has many" 관계를 갖는다고 가정하겠습니다.

이제 ActivityFeed 인스턴스들을 조회하면서 각 인스턴스의 parentable 부모 모델을 즉시 로딩하는 동시에, 부모가 사진(Photo)이면 연결된 태그 개수를, 부모가 게시글(Post)이면 연결된 댓글 개수를 함께 가져오고 싶다고 해봅시다:

use Illuminate\Database\Eloquent\Relations\MorphTo; $activities = ActivityFeed::with([ 'parentable' => function (MorphTo $morphTo) { $morphTo->morphWithCount([ Photo::class => ['tags'], Post::class => ['comments'], ]); }])->get();

나중에 개수 로드하기 (지연 카운트 로딩)

이미 ActivityFeed 모델 목록을 조회해둔 상태에서, 각 액티비티에 연결된 parentable 모델들의 하위 관계 개수를 나중에 로드하고 싶은 경우도 있습니다. 이럴 때는 loadMorphCount 메서드를 사용하면 됩니다:

$activities = ActivityFeed::with('parentable')->get(); $activities->loadMorphCount('parentable', [ Photo::class => ['tags'], Post::class => ['comments'], ]);

Eager Loading (즉시 로딩)

Eloquent 관계를 프로퍼티처럼 접근하면 관련 모델은 "지연 로딩(lazy loading)"됩니다. 즉, 해당 프로퍼티에 처음 접근하는 시점이 되어서야 실제로 관계 데이터를 조회한다는 뜻입니다. 하지만 Eloquent는 부모 모델을 조회하는 시점에 관계를 미리 로딩하는 "즉시 로딩(eager loading)"도 지원합니다. 즉시 로딩을 사용하면 흔히 발생하는 "N + 1 쿼리 문제"를 해결할 수 있습니다.

N + 1 쿼리 문제가 무엇인지 예를 들어보겠습니다. Author 모델에 속하는(belongsTo) Book 모델이 있다고 가정해봅시다:

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Book extends Model { /** * 이 책을 쓴 저자를 가져옵니다. */ public function author(): BelongsTo { return $this->belongsTo(Author::class); } }

이제 모든 책과 각 책의 저자를 함께 조회해보겠습니다:

use App\Models\Book; $books = Book::all(); foreach ($books as $book) { echo $book->author->name; }

이 반복문은 먼저 모든 책을 조회하는 쿼리 1번을 실행한 뒤, 각 책마다 저자를 조회하는 쿼리를 추가로 실행합니다. 만약 책이 25권이라면 위 코드는 총 26번의 쿼리를 실행하게 됩니다. 책 목록 조회 1번 + 각 책의 저자 조회 25번, 이것이 바로 "N + 1 쿼리 문제"입니다.

다행히 즉시 로딩을 사용하면 이 작업을 단 2번의 쿼리로 줄일 수 있습니다. 쿼리를 작성할 때 with 메서드를 사용해 즉시 로딩할 관계를 지정할 수 있습니다:

$books = Book::with('author')->get(); foreach ($books as $book) { echo $book->author->name; }

이렇게 하면 책 전체를 조회하는 쿼리 1번과, 조회된 모든 책의 저자를 한 번에 가져오는 쿼리 1번, 총 2번의 쿼리만 실행됩니다:

select * from books select * from authors where id in (1, 2, 3, 4, 5, ...)

NOTE

실무에서는 관계 개수가 적을 때는 N + 1 문제가 눈에 띄지 않다가, 데이터가 많아지면서 갑자기 페이지 응답이 느려지는 경우가 흔합니다. laravel-debugbar나 텔레스코프(Telescope)를 사용하면 실행된 쿼리 수를 쉽게 확인할 수 있으니, 목록 화면을 만들 때마다 습관적으로 점검해보는 것을 추천합니다.

여러 관계 즉시 로딩하기

여러 관계를 한 번에 즉시 로딩해야 할 때는 with 메서드에 배열을 전달하면 됩니다:

$books = Book::with(['author', 'publisher'])->get();

중첩 관계 즉시 로딩하기

관계의 관계(중첩 관계)까지 즉시 로딩하려면 "점(dot) 표기법"을 사용할 수 있습니다. 예를 들어, 책의 저자와 그 저자의 연락처 정보까지 함께 즉시 로딩해보겠습니다:

$books = Book::with('author.contacts')->get();

또는 with 메서드에 중첩 배열을 전달하는 방식으로도 중첩 관계를 지정할 수 있습니다. 여러 중첩 관계를 한 번에 로딩할 때 이 방식이 더 편리할 수 있습니다:

$books = Book::with([ 'author' => [ 'contacts', 'publisher', ], ])->get();

`morphTo` 관계의 중첩 즉시 로딩

morphTo 관계를 즉시 로딩하면서, 그 관계가 반환할 수 있는 다양한 엔티티들의 중첩 관계까지 함께 로딩하고 싶다면 with 메서드와 morphTo 관계의 morphWith 메서드를 함께 사용할 수 있습니다. 이해를 돕기 위해 다음 모델을 살펴보겠습니다:

<?php use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; class ActivityFeed extends Model { /** * 활동 피드 레코드의 부모 모델을 가져옵니다. */ public function parentable(): MorphTo { return $this->morphTo(); } }

이 예제에서 Event, Photo, Post 모델이 각각 ActivityFeed 모델을 생성할 수 있다고 가정합니다. 또한 Event 모델은 Calendar 모델에 속하고, Photo 모델은 Tag 모델과 연관되어 있으며, Post 모델은 Author 모델에 속한다고 가정합니다.

이런 모델 정의와 관계를 바탕으로, ActivityFeed 모델 인스턴스를 조회하면서 parentable 모델과 그에 딸린 중첩 관계까지 모두 즉시 로딩할 수 있습니다:

use Illuminate\Database\Eloquent\Relations\MorphTo; $activities = ActivityFeed::query() ->with(['parentable' => function (MorphTo $morphTo) { $morphTo->morphWith([ Event::class => ['calendar'], Photo::class => ['tags'], Post::class => ['author'], ]); }])->get();

특정 컬럼만 즉시 로딩하기

관계로 가져오는 모든 컬럼이 항상 필요한 것은 아닙니다. Eloquent는 즉시 로딩할 관계에서 원하는 컬럼만 선택적으로 가져올 수 있게 지원합니다:

$books = Book::with('author:id,name,book_id')->get();

WARNING

이 기능을 사용할 때는 조회할 컬럼 목록에 반드시 id 컬럼과 관련된 외래 키 컬럼을 포함해야 합니다.

기본적으로 항상 즉시 로딩하기

모델을 조회할 때마다 특정 관계를 항상 함께 로딩하고 싶다면, 모델에 $with 프로퍼티를 정의하면 됩니다:

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Book extends Model { /** * 항상 즉시 로딩할 관계 목록입니다. * * @var array */ protected $with = ['author']; /** * 이 책을 쓴 저자를 가져옵니다. */ public function author(): BelongsTo { return $this->belongsTo(Author::class); } /** * 이 책의 장르를 가져옵니다. */ public function genre(): BelongsTo { return $this->belongsTo(Genre::class); } }

특정 쿼리 하나에서만 $with 프로퍼티에 지정된 항목을 제외하고 싶다면 without 메서드를 사용하세요:

$books = Book::without('author')->get();

특정 쿼리 하나에서 $with 프로퍼티에 지정된 항목 전체를 다른 값으로 덮어쓰고 싶다면 withOnly 메서드를 사용하세요:

$books = Book::withOnly('genre')->get();

즉시 로딩 시 조건 지정하기

관계를 즉시 로딩하면서 해당 로딩 쿼리에 추가 조건을 걸고 싶을 때가 있습니다. 이 경우 with 메서드에 배열을 전달하되, 배열의 키는 관계 이름으로, 값은 로딩 쿼리에 조건을 추가하는 클로저로 지정하면 됩니다:

use App\Models\User; $users = User::with(['posts' => function ($query) { $query->where('title', 'like', '%코드%'); }])->get();

이 예제에서는 게시글의 title 컬럼에 "코드"라는 단어가 포함된 게시글만 즉시 로딩합니다. 이 외에도 쿼리 빌더의 다른 메서드를 호출해서 즉시 로딩 쿼리를 더 세밀하게 다듬을 수 있습니다:

$users = User::with(['posts' => function ($query) { $query->orderBy('created_at', 'desc'); }])->get();

`morphTo` 관계의 즉시 로딩에 조건 지정하기

morphTo 관계를 즉시 로딩하는 경우, Eloquent는 관련된 각 모델 타입마다 별도의 쿼리를 실행합니다. MorphTo 관계의 constrain 메서드를 사용하면 이런 각각의 쿼리에 조건을 추가할 수 있습니다:

use Illuminate\Database\Eloquent\Relations\MorphTo; $comments = Comment::with(['commentable' => function (MorphTo $morphTo) { $morphTo->constrain([ Post::class => function ($query) { $query->whereNull('hidden_at'); }, Video::class => function ($query) { $query->where('type', 'educational'); }, ]); }])->get();

이 예제에서는 숨김 처리되지 않은 게시글과, type이 "educational"인 동영상만 즉시 로딩됩니다.

특정 조건을 만족하는 관계가 존재하는지 확인함과 동시에, 같은 조건으로 해당 관계를 즉시 로딩하고 싶은 경우가 있습니다. 예를 들어 특정 조건에 맞는 Post 모델을 가진 User 모델만 조회하면서, 동시에 해당 조건에 맞는 게시글도 함께 즉시 로딩하고 싶을 수 있습니다. 이럴 때는 withWhereHas 메서드를 사용하면 됩니다:

use App\Models\User; $users = User::withWhereHas('posts', function ($query) { $query->where('featured', true); })->get();

Lazy Eager Loading (지연 즉시 로딩)

부모 모델을 이미 조회한 뒤에 관계를 즉시 로딩해야 하는 경우도 있습니다. 예를 들어 조건에 따라 관련 모델을 로딩할지 동적으로 결정해야 할 때 유용합니다:

use App\Models\Book; $books = Book::all(); if ($condition) { $books->load('author', 'publisher'); }

즉시 로딩 쿼리에 추가 조건을 걸고 싶다면, 로딩할 관계를 키로 하고 값으로 쿼리 인스턴스를 받는 클로저를 담은 배열을 전달하면 됩니다:

$author->load(['books' => function ($query) { $query->orderBy('published_date', 'asc'); }]);

아직 로딩되지 않은 경우에만 관계를 로딩하고 싶다면 loadMissing 메서드를 사용하세요:

$book->loadMissing('author');

중첩 관계의 Lazy Eager Loading과 `morphTo`

morphTo 관계와 그 관계가 반환할 수 있는 다양한 엔티티들의 중첩 관계를 함께 지연 즉시 로딩하고 싶다면 loadMorph 메서드를 사용할 수 있습니다.

이 메서드는 첫 번째 인수로 morphTo 관계의 이름을, 두 번째 인수로 모델과 관계를 짝지은 배열을 받습니다. 이해를 돕기 위해 다음 모델을 살펴보겠습니다:

<?php use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; class ActivityFeed extends Model { /** * 활동 피드 레코드의 부모 모델을 가져옵니다. */ public function parentable(): MorphTo { return $this->morphTo(); } }

이 예제에서 Event, Photo, Post 모델이 각각 ActivityFeed 모델을 생성할 수 있다고 가정합니다. 또한 Event 모델은 Calendar 모델에 속하고, Photo 모델은 Tag 모델과 연관되어 있으며, Post 모델은 Author 모델에 속한다고 가정합니다.

이런 모델 정의와 관계를 바탕으로, ActivityFeed 모델 인스턴스를 조회한 뒤 parentable 모델과 그에 딸린 중첩 관계까지 모두 지연 즉시 로딩할 수 있습니다:

$activities = ActivityFeed::with('parentable') ->get() ->loadMorph('parentable', [ Event::class => ['calendar'], Photo::class => ['tags'], Post::class => ['author'], ]);

자동 즉시 로딩

대부분의 경우 라라벨은 여러분이 접근하는 관계를 자동으로 즉시 로딩할 수 있습니다. 이 기능을 활성화하려면 애플리케이션의 AppServiceProviderboot 메서드에서 Model::automaticallyEagerLoadRelationships 메서드를 호출하면 됩니다:

use Illuminate\Database\Eloquent\Model; /** * 애플리케이션의 서비스를 부트스트랩합니다. */ public function boot(): void { Model::automaticallyEagerLoadRelationships(); }

이 기능을 활성화하면, 라라벨은 아직 로딩되지 않은 관계에 접근할 때 자동으로 이를 로딩하려고 시도합니다. 예를 들어 다음 코드를 살펴보겠습니다:

use App\Models\User; $users = User::all(); foreach ($users as $user) { foreach ($user->posts as $post) { foreach ($post->comments as $comment) { echo $comment->content; } } }

일반적으로 위 코드는 각 사용자마다 게시글을 조회하는 쿼리 1번, 각 게시글마다 댓글을 조회하는 쿼리 1번씩을 실행합니다. 하지만 automaticallyEagerLoadRelationships 기능을 활성화하면, 조회된 사용자 컬렉션 중 어느 한 사용자의 게시글에 접근하는 순간 라라벨이 전체 사용자의 게시글을 한꺼번에 지연 즉시 로딩합니다. 마찬가지로 조회된 게시글 중 어느 하나의 댓글에 접근하는 순간, 처음 조회했던 모든 게시글의 댓글이 한꺼번에 지연 즉시 로딩됩니다.

이 기능을 전역적으로 활성화하고 싶지 않다면, 개별 Eloquent 컬렉션 인스턴스에서 withRelationshipAutoloading 메서드를 호출해 해당 컬렉션에만 이 기능을 적용할 수도 있습니다:

$users = User::where('vip', true)->get(); return $users->withRelationshipAutoloading();

지연 로딩 방지하기

앞서 살펴본 것처럼 관계를 즉시 로딩하면 애플리케이션 성능에 상당한 이점을 얻을 수 있습니다. 그래서 필요하다면 관계의 지연 로딩 자체를 항상 금지하도록 라라벨에 지시할 수도 있습니다. 이를 위해서는 Eloquent 기본 모델 클래스가 제공하는 preventLazyLoading 메서드를 호출하면 됩니다. 보통은 애플리케이션의 AppServiceProvider 클래스의 boot 메서드 안에서 이 메서드를 호출합니다.

preventLazyLoading 메서드는 지연 로딩을 막을지 여부를 나타내는 boolean 값을 선택적으로 받을 수 있습니다. 예를 들어, 운영 환경이 아닐 때만 지연 로딩을 비활성화하도록 설정하면, 실수로 지연 로딩되는 관계가 코드에 남아있더라도 운영 환경에서는 정상적으로 동작하게 만들 수 있습니다:

use Illuminate\Database\Eloquent\Model; /** * 애플리케이션의 서비스를 부트스트랩합니다. */ public function boot(): void { Model::preventLazyLoading(! $this->app->isProduction()); }

지연 로딩을 금지한 상태에서 애플리케이션이 Eloquent 관계를 지연 로딩하려고 시도하면, Eloquent는 Illuminate\Database\LazyLoadingViolationException 예외를 발생시킵니다.

handleLazyLoadingViolationsUsing 메서드를 사용하면 지연 로딩 위반이 발생했을 때의 동작을 원하는 대로 커스터마이징할 수 있습니다. 예를 들어, 다음과 같이 하면 지연 로딩 위반이 발생해도 애플리케이션 실행을 예외로 중단시키는 대신 로그만 남기도록 만들 수 있습니다:

Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) { $class = $model::class; info("모델 [{$class}]에서 관계 [{$relation}]을(를) 지연 로딩하려고 시도했습니다."); });

NOTE

개발 환경에서 preventLazyLoading을 활성화해두면, 무심코 즉시 로딩을 빠뜨린 코드를 개발 단계에서 바로 발견할 수 있어 매우 유용합니다. 팀 컨벤션으로 AppServiceProvider에 미리 설정해두는 것을 권장합니다.

관련 모델 삽입 및 수정

`save` 메서드

Eloquent는 관계에 새 모델을 손쉽게 추가할 수 있는 편리한 메서드들을 제공합니다. 예를 들어 게시글(Post)에 새 댓글(Comment)을 추가한다고 가정해봅시다. Comment 모델의 post_id 속성을 직접 설정하는 대신, 관계의 save 메서드를 사용해 댓글을 삽입할 수 있습니다.

use App\Models\Comment; use App\Models\Post; $comment = new Comment(['message' => 'A new comment.']); $post = Post::find(1); $post->comments()->save($comment);

여기서 comments를 동적 프로퍼티로 접근하지 않고, comments() 메서드를 호출해 관계 인스턴스를 얻었다는 점에 주목하세요. save 메서드는 새로 생성되는 Comment 모델에 적절한 post_id 값을 자동으로 채워줍니다.

관련된 모델을 여러 개 한 번에 저장해야 한다면 saveMany 메서드를 사용할 수 있습니다.

$post = Post::find(1); $post->comments()->saveMany([ new Comment(['message' => 'A new comment.']), new Comment(['message' => 'Another new comment.']), ]);

savesaveMany 메서드는 전달받은 모델 인스턴스를 데이터베이스에 저장하지만, 부모 모델에 이미 로드되어 메모리에 올라와 있는 관계에는 새로 저장된 모델을 자동으로 추가하지 않습니다. savesaveMany를 호출한 후 해당 관계에 접근할 계획이라면, refresh 메서드로 모델과 관계를 다시 불러오는 것이 좋습니다.

$post->comments()->save($comment); $post->refresh(); // 새로 저장된 댓글을 포함한 모든 댓글... $post->comments;

모델과 관계를 재귀적으로 저장하기

모델과 그에 연관된 모든 관계를 한 번에 save하고 싶다면 push 메서드를 사용하세요. 아래 예시에서는 Post 모델뿐 아니라 그 댓글들, 그리고 각 댓글 작성자까지 함께 저장됩니다.

$post = Post::find(1); $post->comments[0]->message = 'Message'; $post->comments[0]->author->name = 'Author Name'; $post->push();

pushQuietly 메서드를 사용하면 이벤트를 발생시키지 않고 모델과 연관된 관계를 저장할 수 있습니다.

$post->pushQuietly();

`create` 메서드

save, saveMany 메서드 외에도 create 메서드를 사용할 수 있습니다. 이 메서드는 속성 배열을 받아 모델을 생성하고 데이터베이스에 삽입합니다. savecreate의 차이는, save는 완전한 Eloquent 모델 인스턴스를 받는 반면 create는 일반 PHP 배열(array)을 받는다는 점입니다. create 메서드는 새로 생성된 모델을 반환합니다.

use App\Models\Post; $post = Post::find(1); $comment = $post->comments()->create([ 'message' => 'A new comment.', ]);

여러 관련 모델을 한 번에 생성하려면 createMany 메서드를 사용하세요.

$post = Post::find(1); $post->comments()->createMany([ ['message' => 'A new comment.'], ['message' => 'Another new comment.'], ]);

이벤트를 발생시키지 않고 모델을 생성하고 싶다면 createQuietlycreateManyQuietly 메서드를 사용할 수 있습니다.

$user = User::find(1); $user->posts()->createQuietly([ 'title' => 'Post title.', ]); $user->posts()->createManyQuietly([ ['title' => 'First post.'], ['title' => 'Second post.'], ]);

관계 위에서 모델을 생성하거나 업데이트할 때 findOrNew, firstOrNew, firstOrCreate, updateOrCreate 메서드도 활용할 수 있습니다. 자세한 내용은 모델 upsert 문서를 참고하세요.

NOTE

create 메서드를 사용하기 전에 대량 할당(mass assignment) 문서를 반드시 확인하세요.

Belongs To 관계

자식 모델을 새로운 부모 모델에 연결하고 싶다면 associate 메서드를 사용하세요. 아래 예시에서 User 모델은 Account 모델에 대한 belongsTo 관계를 정의하고 있습니다. associate 메서드는 자식 모델에 외래 키를 설정해줍니다.

use App\Models\Account; $account = Account::find(10); $user->account()->associate($account); $user->save();

반대로 자식 모델에서 부모 모델과의 연결을 해제하려면 dissociate 메서드를 사용하세요. 이 메서드는 관계의 외래 키를 null로 설정합니다.

$user->account()->dissociate(); $user->save();

다대다 관계

Attach / Detach

Eloquent는 다대다 관계를 더 편리하게 다룰 수 있는 메서드들도 제공합니다. 사용자가 여러 역할(Role)을 가질 수 있고, 역할도 여러 사용자에게 속할 수 있는 상황을 예로 들어보겠습니다. attach 메서드를 사용하면 중간 테이블(intermediate table)에 레코드를 삽입하여 사용자에게 역할을 연결할 수 있습니다.

use App\Models\User; $user = User::find(1); $user->roles()->attach($roleId);

관계를 연결할 때 중간 테이블에 함께 삽입할 추가 데이터를 배열로 전달할 수도 있습니다.

$user->roles()->attach($roleId, ['expires' => $expires]);

때로는 사용자에게서 역할을 제거해야 할 수도 있습니다. 다대다 관계 레코드를 제거하려면 detach 메서드를 사용하세요. detach 메서드는 중간 테이블에서 해당 레코드만 삭제하며, 두 모델 자체는 데이터베이스에 그대로 남습니다.

// 사용자에게서 역할 하나만 detach... $user->roles()->detach($roleId); // 사용자의 모든 역할을 detach... $user->roles()->detach();

편의를 위해 attachdetach는 ID 배열도 입력으로 받을 수 있습니다.

$user = User::find(1); $user->roles()->detach([1, 2, 3]); $user->roles()->attach([ 1 => ['expires' => $expires], 2 => ['expires' => $expires], ]);

연관 관계 동기화(Sync)

sync 메서드를 사용해서도 다대다 연관 관계를 구성할 수 있습니다. sync 메서드는 중간 테이블에 위치시킬 ID 배열을 인수로 받습니다. 주어진 배열에 없는 ID는 중간 테이블에서 제거됩니다. 즉, 이 작업이 끝나면 중간 테이블에는 배열에 주어진 ID만 남게 됩니다.

$user->roles()->sync([1, 2, 3]);

ID와 함께 중간 테이블에 저장할 추가 값을 전달할 수도 있습니다.

$user->roles()->sync([1 => ['expires' => true], 2, 3]);

동기화되는 모든 모델 ID에 동일한 중간 테이블 값을 함께 삽입하고 싶다면 syncWithPivotValues 메서드를 사용하세요.

$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);

주어진 배열에 없는 기존 ID를 detach하고 싶지 않다면 syncWithoutDetaching 메서드를 사용하세요.

$user->roles()->syncWithoutDetaching([1, 2, 3]);

연관 관계 토글(Toggle)

다대다 관계는 주어진 관련 모델 ID들의 연결 상태를 "토글"하는 toggle 메서드도 제공합니다. 주어진 ID가 현재 연결되어 있다면 detach되고, 반대로 연결되어 있지 않다면 attach됩니다.

$user->roles()->toggle([1, 2, 3]);

ID와 함께 중간 테이블에 저장할 추가 값을 전달할 수도 있습니다.

$user->roles()->toggle([ 1 => ['expires' => true], 2 => ['expires' => true], ]);

트랜잭션 기반 Pivot 작업

앞서 설명한 각 pivot 작업에는 OrFail 버전(attachOrFail, detachOrFail, syncOrFail, syncWithoutDetachingOrFail, toggleOrFail)도 존재합니다. 이 메서드들은 작업을 데이터베이스 트랜잭션으로 감싸서, 예외가 발생하면 모든 변경 사항이 자동으로 롤백되도록 합니다.

$user->roles()->attachOrFail([1, 2, 3]); $user->roles()->syncOrFail([1, 2, 3]);

중간 테이블 레코드 업데이트하기

관계의 중간 테이블에 있는 기존 레코드를 수정해야 한다면 updateExistingPivot 메서드를 사용하세요. 이 메서드는 중간 레코드의 외래 키와 업데이트할 속성 배열을 인수로 받습니다.

$user = User::find(1); $user->roles()->updateExistingPivot($roleId, [ 'active' => false, ]);

부모 모델의 타임스탬프 갱신하기

CommentPost에 속하는 경우처럼, 어떤 모델이 다른 모델과 belongsTobelongsToMany 관계를 맺고 있을 때, 자식 모델이 업데이트되면 부모 모델의 타임스탬프도 함께 갱신하고 싶은 경우가 있습니다.

예를 들어 Comment 모델이 업데이트될 때, 이 댓글이 속한 Postupdated_at 타임스탬프도 자동으로 현재 시각으로 갱신("touch")하고 싶을 수 있습니다. 이럴 때는 자식 모델에 Touches 어트리뷰트를 추가하고, 자식 모델이 업데이트될 때 updated_at 타임스탬프를 함께 갱신할 관계의 이름을 지정하면 됩니다:

<?php namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Touches; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; #[Touches(['post'])] class Comment extends Model { /** * 이 댓글이 속한 게시글을 반환합니다. */ public function post(): BelongsTo { return $this->belongsTo(Post::class); } }

WARNING

부모 모델의 타임스탬프는 자식 모델이 Eloquent의 save 메서드를 통해 업데이트될 때만 함께 갱신됩니다.

이 기능은 예를 들어 게시글 목록을 "최근 활동 순"으로 정렬해서 보여줄 때 유용합니다. 댓글이 새로 달릴 때마다 해당 게시글의 updated_at이 갱신되므로, 별도의 추가 로직 없이도 최근에 댓글이 달린 게시글이 자연스럽게 상위에 노출되도록 만들 수 있습니다.

이 문서는 Laravel 공식 문서(MIT)를 한국 개발자를 위해 번역·재구성한 것입니다.

번역일: 2026년 9월 17일