Eloquent: 관계 (Relationships)
번역일: 2026년 6월 25일
Eloquent: 관계 (Relationships)
- 소개
- 관계 정의하기
- 다대다 관계 (Many to Many)
- 다형성 관계 (Polymorphic Relationships)
- 동적 관계 (Dynamic Relationships)
- 관계 쿼리
- 연관 모델 집계
- Eager 로딩
- 연관 모델 삽입 및 수정
- 부모 타임스탬프 갱신 (Touching)
소개
데이터베이스 테이블은 서로 연관된 경우가 많습니다. 예를 들어 블로그 게시글에는 여러 댓글이 달릴 수 있고, 주문은 해당 주문을 생성한 사용자와 연결됩니다. Eloquent는 이러한 관계를 쉽게 정의하고 다룰 수 있도록 다양한 관계 타입을 지원합니다.
- 일대일 (One To One)
- 일대다 (One To Many)
- 다대다 (Many To Many)
- Has One Through
- Has Many Through
- 일대일 다형성 (One To One Polymorphic)
- 일대다 다형성 (One To Many Polymorphic)
- 다대다 다형성 (Many To Many Polymorphic)
관계 정의하기
Eloquent 관계는 Eloquent 모델 클래스의 메서드로 정의합니다. 관계는 강력한 쿼리 빌더 역할도 하므로, 메서드로 정의하면 메서드 체이닝과 쿼리 조합을 유연하게 활용할 수 있습니다. 예를 들어, posts 관계에 추가 쿼리 조건을 이어붙일 수 있습니다.
$user->posts()->where('active', 1)->get();
각 관계 타입을 어떻게 정의하는지 하나씩 살펴보겠습니다.
일대일 (One to 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 외래 키가 있다고 가정합니다. 이 규칙을 변경하려면 hasOne의 두 번째 인수로 외래 키명을 전달하세요.
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를 붙여 외래 키명을 자동으로 추론합니다. 외래 키명이 다를 경우 두 번째 인수로 지정합니다.
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');
}일대다 (One to 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를 붙입니다. 이 예시에서는 Comment 모델의 외래 키가 post_id라고 가정합니다.
관계 메서드를 정의하면 동적 프로퍼티로 관련 댓글 컬렉션에 접근할 수 있습니다.
use App\Models\Post;
$comments = Post::find(1)->comments;
foreach ($comments as $comment) {
// ...
}관계도 쿼리 빌더 역할을 하므로 메서드로 호출하여 조건을 추가할 수 있습니다.
$comment = Post::find(1)->comments()
->where('title', 'foo')
->first();hasOne과 마찬가지로 외래 키와 로컬 키를 추가 인수로 재정의할 수 있습니다.
return $this->hasMany(Comment::class, 'foreign_key');
return $this->hasMany(Comment::class, 'foreign_key', 'local_key');일대다 역방향 / 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는 관계 메서드명을 기반으로 외래 키를 결정합니다. 위 예시에서는 post_id를 외래 키로 가정합니다.
외래 키가 관례와 다를 경우 두 번째 인수로, 부모 모델의 기본 키가 id가 아닐 경우 세 번째 인수로 지정합니다.
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key');
}
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}기본 모델 (Default Models)
belongsTo, hasOne, hasOneThrough, morphOne 관계에서는 관계가 null일 때 반환할 기본 모델을 지정할 수 있습니다. 이 패턴은 Null Object 패턴이라고 하며, 코드에서 불필요한 null 체크를 줄여줍니다. 다음 예시에서 Post 모델에 사용자가 연결되어 있지 않으면 빈 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();
}가장 오래된 연관 모델을 조회하는 메서드도 정의할 수 있습니다.
/**
* 사용자의 가장 오래된 주문을 반환합니다.
*/
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 컬럼과 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 모델과 연결되어 있고, 새 가격이 등록되더라도 기존 가격 데이터는 유지됩니다. 또한 published_at 컬럼을 통해 미래 날짜로 가격을 미리 등록할 수도 있습니다.
이런 경우, 현재 시점 이전에 게시된 가격 중 가장 최근 것(동일 날짜라면 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" 관계는 중간 모델을 거쳐서 다른 모델과 일대일로 연결되는 관계입니다.
예를 들어, 자동차 정비소 애플리케이션에서 각 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);
}
}첫 번째 인수는 최종적으로 접근할 모델, 두 번째 인수는 중간 모델입니다.
관련 모델에 이미 관계가 정의되어 있다면, 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 테이블의 로컬 키
);
}
}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);
}
}관련 모델에 이미 관계가 정의되어 있다면, 유연한 방식으로도 정의할 수 있습니다.
// 문자열 기반 문법
return $this->through('environments')->has('deployments');
// 동적 문법
return $this->throughEnvironments()->hasDeployments();Deployment 모델 테이블에는 project_id 컬럼이 없지만, hasManyThrough 관계를 통해 $project->deployments로 접근할 수 있습니다. Eloquent는 중간 Environment 테이블의 project_id를 먼저 확인한 뒤, 해당 환경 ID를 사용해 Deployment 테이블을 조회합니다.
키 관례
키를 직접 지정하려면 hasManyThrough에 추가 인수를 전달합니다.
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 테이블의 로컬 키
);
}
}다대다 관계 (Many to Many)
다대다 관계는 hasOne이나 hasMany보다 조금 더 복잡합니다. 예를 들어, 사용자는 여러 역할(Role)을 가질 수 있고, 하나의 역할은 여러 사용자가 공유할 수 있습니다.
테이블 구조
이 관계를 정의하려면 세 개의 테이블이 필요합니다. users, roles, 그리고 중간 테이블인 role_user입니다. role_user 테이블은 연관된 두 모델명을 알파벳순으로 합쳐 명명하며, user_id와 role_id 컬럼을 포함합니다.
users
id - integer
name - string
roles
id - integer
name - string
role_user
user_id - integer
role_id - integer모델 구조
다대다 관계는 belongsToMany 메서드를 반환하는 메서드로 정의합니다.
<?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);
}
}관계를 정의하면 동적 프로퍼티로 역할 목록에 접근할 수 있습니다.
use App\Models\User;
$user = User::find(1);
foreach ($user->roles as $role) {
// ...
}쿼리 빌더로 활용할 수도 있습니다.
$roles = User::find(1)->roles()->orderBy('name')->get();
중간 테이블명은 두 모델명을 알파벳순으로 합쳐 자동 결정됩니다. 직접 지정하려면 두 번째 인수를 전달하세요