Eloquent: 관계 (Relationships)

번역일: 2026년 6월 25일

Eloquent: 관계 (Relationships)

소개

데이터베이스 테이블은 대개 서로 연관되어 있습니다. 예를 들어 블로그 게시글에는 여러 댓글이 달릴 수 있고, 주문은 해당 주문을 작성한 사용자와 연결됩니다. Eloquent는 이러한 관계를 쉽게 관리하고 다룰 수 있도록 해주며, 아래의 관계 유형을 지원합니다.

관계 정의하기

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 외래 키가 있다고 가정합니다. 이 규칙을 변경하려면 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를 붙여 외래 키 이름을 자동으로 결정합니다. 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를 붙입니다. 이 예에서는 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');

자식 모델에 부모 모델 자동 주입하기

Eager 로딩을 사용하더라도, 자식 모델을 순회하면서 부모 모델에 접근하면 N+1 쿼리 문제가 발생할 수 있습니다.

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

위 예에서 댓글은 Eager 로딩되었지만, 각 Comment 모델에 부모 Post가 자동으로 주입되지 않기 때문에 N+1 쿼리 문제가 발생합니다.

이를 방지하려면 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(); } }

또는 Eager 로딩 시점에 런타임으로 적용할 수도 있습니다.

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

일대다 (역방향) / 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); } }

관계가 정의되면 동적 프로퍼티로 부모 게시글에 접근할 수 있습니다.

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

Eloquent는 관계 메서드 이름 뒤에 _와 부모 모델의 기본 키 컬럼 이름을 붙여 외래 키를 결정합니다. 이 예에서는 comments 테이블의 외래 키가 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 체크를 줄이는 데 도움이 됩니다. 아래 예에서 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 절을 작성할 수도 있지만, whereBelongsTo 메서드를 사용하면 관계와 외래 키를 자동으로 판별해줍니다.

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

컬렉션 인스턴스를 전달하면 해당 컬렉션에 속한 모든 부모 모델의 자식을 조회합니다.

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

기본적으로 모델의 클래스명을 기반으로 관계를 결정하지만, 두 번째 인자로 관계명을 직접 지정할 수 있습니다.

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

Has One of Many

모델이 여러 연관 모델을 가지는 상황에서, "가장 최신" 또는 "가장 오래된" 연관 모델 하나만 편리하게 가져오고 싶을 때가 있습니다. 예를 들어 User 모델이 여러 Order 모델과 연관될 때, 가장 최근 주문 하나만 조회하는 편리한 방법이 필요할 수 있습니다. hasOneofMany 메서드를 조합하면 이를 구현할 수 있습니다.

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

가장 오래된 연관 모델도 비슷하게 정의할 수 있습니다.

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

기본적으로 latestOfManyoldestOfMany는 기본 키를 기준으로 정렬합니다. 다른 정렬 기준이 필요하다면 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 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'); }

HasManyThrough 관계도 같은 방식으로 HasOneThrough로 변환할 수 있습니다.

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

고급 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 - 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); } }

첫 번째 인자는 최종적으로 접근할 모델, 두 번째 인자는 중간 모델입니다.

관련 모델에 이미 관계가 정의되어 있다면 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 메서드 체이닝을 사용합니다.

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

Has Many Through

"has-many-through" 관계는 중간 모델을 통해 멀리 있는 모델에 편리하게 접근할 수 있게 해줍니다. 예를 들어 배포 플랫폼에서 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); } }

기존 관계를 재사용하는 방식도 사용할 수 있습니다.

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

Deployment 모델의 테이블에 application_id

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

번역일: 2026년 6월 25일