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 외래 키가 있다고 가정합니다. 이 규칙을 재정의하려면 두 번째 인수로 외래 키명을 전달합니다.

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와 연결되어 있지만, 가장 최근 주문만 편리하게 가져오고 싶다면 hasOneofMany 메서드를 조합합니다.

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

가장 오래된 연관 모델을 가져오려면 oldestOfMany를 사용합니다.

/** * 사용자의 가장 오래된 주문을 가져옵니다. */ 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 컬럼과 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 - 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 메서드를 사용해 유연하게 정의할 수도 있습니다.

// 문자열 방식 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 - string

Project 모델에 관계를 정의합니다.

<?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

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

번역일: 2026년 6월 25일