Eloquent: 관계 (Relationships)
번역일: 2026년 6월 25일
Eloquent: 관계 (Relationships)
- 소개
- 관계 정의하기
- 스코프 관계
- 다대다 관계
- 다형성 관계
- 동적 관계
- 관계 쿼리하기
- 연관 모델 집계
- Eager 로딩
- 연관 모델 삽입 및 수정
- 부모 타임스탬프 갱신 (Touching)
소개
데이터베이스 테이블은 대부분 서로 연관되어 있습니다. 예를 들어 블로그 게시글에는 여러 댓글이 달릴 수 있고, 주문은 주문한 사용자와 연결됩니다. Eloquent는 이러한 관계를 쉽게 정의하고 다룰 수 있도록 해주며, 다음과 같은 다양한 관계 유형을 지원합니다.
- 일대일 (One To One)
- 일대다 (One To Many)
- 다대다 (Many To Many)
- Has One Through
- Has Many Through
- 일대일 (다형성)
- 일대다 (다형성)
- 다대다 (다형성)
관계 정의하기
Eloquent 관계는 모델 클래스의 메서드로 정의합니다. 관계는 강력한 쿼리 빌더 역할도 하기 때문에, 메서드로 정의하면 메서드 체이닝과 쿼리 기능을 함께 활용할 수 있습니다. 예를 들어 posts 관계에 추가 조건을 체이닝할 수 있습니다.
$user->posts()->where('active', 1)->get();
본격적으로 사용하기 전에, 각 관계 유형을 어떻게 정의하는지 살펴보겠습니다.
일대일 / Has One
일대일 관계는 가장 기본적인 관계 유형입니다. 예를 들어 User 모델이 하나의 Phone 모델과 연결될 수 있습니다. 이 관계를 정의하려면 User 모델에 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의 동적 프로퍼티를 통해 연관 레코드에 접근할 수 있습니다. 동적 프로퍼티를 사용하면 관계 메서드를 마치 모델의 일반 프로퍼티처럼 접근할 수 있습니다.
$phone = User::find(1)->phone;
Eloquent는 부모 모델명을 기반으로 외래 키를 자동으로 결정합니다. 이 경우 Phone 모델에 user_id 외래 키가 있다고 가정합니다. 이 규칙을 재정의하려면 두 번째 인수로 외래 키명을 전달합니다.
return $this->hasOne(Phone::class, 'foreign_key');
또한 Eloquent는 외래 키 값이 부모의 기본 키(id)와 일치한다고 가정합니다. 다른 컬럼을 기준으로 삼으려면 세 번째 인수를 사용합니다.
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가 아니라면 두 번째 인수로 커스텀 키명을 전달합니다.
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key');
}부모 모델이 id가 아닌 다른 컬럼을 기본 키로 사용한다면 세 번째 인수로 해당 컬럼을 지정합니다.
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}일대다 / Has Many
일대다 관계는 하나의 모델이 여러 자식 모델을 가지는 관계입니다. 예를 들어 블로그 게시글은 여러 댓글을 가질 수 있습니다. Eloquent 모델에 메서드를 정의하는 방식으로 관계를 선언합니다.
<?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 모델의 외래 키를 자동으로 결정합니다. 관례에 따라 부모 모델명의 스네이크 케이스에 _id를 붙인 post_id가 외래 키로 사용됩니다.
관계 메서드를 정의하면 comments 프로퍼티로 연관 댓글의 컬렉션에 접근할 수 있습니다.
use App\Models\Post;
$comments = Post::find(1)->comments;
foreach ($comments as $comment) {
// ...
}모든 관계는 쿼리 빌더 역할도 하므로, comments 메서드를 호출하고 조건을 체이닝할 수 있습니다.
$comment = Post::find(1)->comments()
->where('title', 'foo')
->first();hasMany에도 두 번째, 세 번째 인수로 외래 키와 로컬 키를 재정의할 수 있습니다.
return $this->hasMany(Comment::class, 'foreign_key');
return $this->hasMany(Comment::class, 'foreign_key', 'local_key');자식 모델에서 부모 모델 자동 하이드레이션
Eager 로딩을 사용하더라도, 자식 모델을 순회하면서 부모 모델에 다시 접근하면 N+1 문제가 발생할 수 있습니다.
$posts = Post::with('comments')->get();
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->post->title; // 각 댓글마다 post를 다시 조회!
}
}위 예시에서는 댓글을 Eager 로드했지만, 각 Comment 모델에 부모 Post가 자동으로 채워지지 않아 N+1 문제가 발생합니다.
hasMany 관계 정의 시 chaperone 메서드를 호출하면 Eloquent가 자식 모델에 부모 모델을 자동으로 채워줍니다.
public function comments(): HasMany
{
return $this->hasMany(Comment::class)->chaperone();
}또는 Eager 로딩 시 런타임에 선택적으로 적용할 수도 있습니다.
$posts = Post::with([
'comments' => fn ($comments) => $comments->chaperone(),
])->get();일대다 (역방향) / Belongs To
게시글의 댓글 목록에 접근할 수 있으니, 이번에는 댓글에서 부모 게시글에 접근하는 역방향 관계를 정의해봅시다. 자식 모델에 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는 관계 메서드명 뒤에 _와 부모 모델의 기본 키 컬럼명을 붙여 외래 키를 결정합니다. 따라서 이 경우 post_id가 기본 외래 키입니다.
외래 키가 이 규칙과 다르다면 두 번째 인수로 커스텀 외래 키를 지정합니다.
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key');
}부모 모델이 id가 아닌 다른 기본 키를 사용한다면 세 번째 인수로 지정합니다.
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}기본 모델
belongsTo, hasOne, hasOneThrough, morphOne 관계에서는 관계가 null일 때 반환할 기본 모델을 정의할 수 있습니다. 이를 Null Object 패턴이라고도 하며, 코드에서 null 확인을 줄일 수 있습니다. 아래 예시에서 게시글에 사용자가 연결되지 않은 경우, 빈 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' => '익명 작성자',
]);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
$user->name = '익명 작성자';
});
}Belongs To 관계 쿼리하기
"belongs to" 관계의 자식을 쿼리할 때 직접 where 절을 작성할 수도 있습니다.
use App\Models\Post;
$posts = Post::where('user_id', $user->id)->get();그러나 whereBelongsTo 메서드를 사용하면 적절한 관계와 외래 키를 자동으로 결정해줘서 더 편리합니다.
$posts = Post::whereBelongsTo($user)->get();
컬렉션 인스턴스를 전달하면 컬렉션 내 모든 부모 모델에 속하는 자식을 가져올 수 있습니다.
$users = User::where('vip', true)->get();
$posts = Post::whereBelongsTo($users)->get();기본적으로 Laravel은 모델의 클래스명을 기반으로 관계를 결정합니다. 관계명을 직접 지정하려면 두 번째 인수로 전달합니다.
$posts = Post::whereBelongsTo($user, 'author')->get();
Has One of Many
모델이 여러 연관 모델을 가지지만, 그 중 "가장 최근" 또는 "가장 오래된" 단 하나만 가져오고 싶을 때 사용합니다. 예를 들어 User는 여러 Order와 연결되어 있지만, 가장 최근 주문만 편리하게 가져오고 싶다면 hasOne과 ofMany 메서드를 조합합니다.
/**
* 사용자의 가장 최근 주문을 가져옵니다.
*/
public function latestOrder(): HasOne
{
return $this->hasOne(Order::class)->latestOfMany();
}가장 오래된 연관 모델을 가져오려면 oldestOfMany를 사용합니다.
/**
* 사용자의 가장 오래된 주문을 가져옵니다.
*/
public function oldestOrder(): HasOne
{
return $this->hasOne(Order::class)->oldestOfMany();
}latestOfMany와 oldestOfMany는 기본적으로 기본 키를 기준으로 정렬합니다. 다른 기준을 사용하려면 ofMany 메서드에 정렬 기준 컬럼과 집계 함수(min 또는 max)를 전달합니다.
/**
* 사용자의 가장 금액이 큰 주문을 가져옵니다.
*/
public function largestOrder(): HasOne
{
return $this->hasOne(Order::class)->ofMany('price', 'max');
}WARNING
PostgreSQL은 UUID 컬럼에 MAX 함수를 지원하지 않으므로, PostgreSQL UUID 컬럼과 Has One of Many 관계를 함께 사용할 수 없습니다.
Has Many 관계를 Has One 관계로 변환하기
latestOfMany, oldestOfMany, ofMany로 단일 모델을 가져올 때 이미 해당 모델에 대한 "has many" 관계가 정의되어 있는 경우가 많습니다. 이때 기존 관계에 one 메서드를 호출해 편리하게 "has one" 관계로 변환할 수 있습니다.
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
public function largestOrder(): HasOne
{
return $this->orders()->one()->ofMany('price', 'max');
}고급 Has One of Many 관계
더 복잡한 "has one of many" 관계도 구성할 수 있습니다. 예를 들어 Product 모델에 여러 Price 모델이 연결되어 있고, 미래 날짜로 예약된 가격도 시스템에 저장된다고 가정해봅시다. 이 경우 현재 시점 이전에 게시된 것 중 가장 최근 가격을 가져와야 합니다. 게시일이 같을 경우에는 id가 큰 것을 우선합니다.
/**
* 상품의 현재 가격을 가져옵니다.
*/
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" 관계는 중간 모델을 거쳐 다른 모델과 일대일로 연결되는 관계입니다.
예를 들어 자동차 정비소 애플리케이션에서 각 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 - integerMechanic 모델에 관계를 정의합니다.
<?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 메서드를 사용해 유연하게 정의할 수도 있습니다.
// 문자열 방식
return $this->through('cars')->has('owner');
// 동적 방식
return $this->throughCars()->hasOwner();키 규칙
키를 커스터마이즈하려면 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 테이블의 로컬 키
);
}
}Has Many Through
"has-many-through" 관계는 중간 모델을 거쳐 멀리 떨어진 모델에 접근하는 방법입니다. 예를 들어 배포 플랫폼을 구축한다면 Project는 중간 Environment를 통해 여러 Deployment에 접근할 수 있습니다.
projects
id - integer
name - string
environments
id - integer
project_id - integer
name - string
deployments
id - integer
environment_id - integer
commit_hash - stringProject 모델에 관계를 정의합니다.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Project extends Model
{
/**
* 프로젝트의 모든 배포를 가져옵니다.
*/
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(Deployment::class, Environment::class);
}
}이미 모델 간 관계가 정의되어 있다면 through 메서드로 유연하게 정의할 수 있습니다.
// 문자열 방식
return $this->through('environments')->has('deployments');
// 동적 방식
return $this->throughEnvironments()->hasDeployments();Deployment 테이블에 project_id 컬럼이 없더라도, hasManyThrough를 통해 $project->deployments로 접근할 수 있습니다. Eloquent는 중간 Environment 테이블에서 관련 environment_id 목록을 찾아 Deployment 테이블을 조회합니다.
키 규칙
키를 커스터마이즈하려면 다음과 같이 추가 인수를 전달합니다.
class Project extends Model
{
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(
Deployment::class,
Environment::class,
'project_id', // environments 테이블의 외래 키
'environment_id', // deployments 테이블의 외래 키
'id', // projects 테이블의 로컬 키
'id' // environments 테이블의 로컬 키
);
}
}스코프 관계
모델에 추가 조건이 붙은 관계 메서드를 정의하는 것은 흔한 패턴입니다. 예를 들어 User 모델에 posts 관계를 확장해 featuredPosts를 정의할 수 있습니다.
<?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);
}
}단순히 where 조건만 추가하면 이 관계를 통해 모델을 생성할 때 featured 속성이 자동으로 설정되지 않습니다. 관계 메서드를 통해 생성되는 모델에도 특정 속성을 자동 부여하려면 withAttributes 메서드를 사용합니다.
public function featuredPosts(): HasMany
{
return $this->posts()->withAttributes(['featured' => true]);
}`with