ejyoo's 개발 노트

명시적 외래키 작성할 때 virtual 타입을 쓰는 이유 본문

BackEnd/.Net Core

명시적 외래키 작성할 때 virtual 타입을 쓰는 이유

ejyoovV 2023. 6. 21. 08:27

EF 의 ORM 을 사용하면서

모델을 설계할 때 FluntAPI 를 사용하여 각 엔티티 모델 간 외래키를 지정하는데,

이때 외래키로서의 식별관계를 가지는 각 모델이 참조할 수 있도록 명시적 외래키 라는 필드를 작성해야 한다.

이때 타입이 virtual로 들어가는데,

그것이 왜 사용되는지 궁금해서 정리하고자 한다.

 

 

 

EF 는 Entitiy Framework 의 약자로 MS에서 개발한 .NET 앱용 오픈소스 객체 관계 매핑 프레임 워크(ORM)이다.

EF 에서 외래키 연관관계를 설정할 때 종종 virtual 키워드를 사용한다.

 

virtual 키워드를 사용하면 EF가 해당 속성을 Lazy Loading을 할 수 있다.

Lazy Loading은 관련 개체가 실제로 필요로 될 때 까지 해당 개체의 로드를 연기하는 패턴이다.

'virtual' 키워드를 사용하게 되면

EF는 해당 속성에 프록시 클래스를 설정하여

처음으로 접근할 때 데이터 베이스에서 해당 속성의 데이터를 로드하게 된다.

 

Lazy Loading 을 사용하기 위한 조건

하지만 virtual 키워드는 EF Core에서 Lazy Loading 을 지원하기 위한 필요 조건 중 하나로 virtual 키워드만 사용한다 해서 LazyLoading을 할 수 있는 것은 아니다.

EF Core에서 Lazy Loading을 사용하려면 아래의 방법을 수행해야 한다.

1. `Microsoft.EntityFrameworkCore.Proxies` 패키지를 설치해야 한다.
2. `UseLazyLoadingProxies()`를 DbContext의 구성에서 호출해야 한다.
3. 관련 엔터티들(즉, 내비게이션 속성)에 `virtual` 키워드를 사용해야 한다.

`virtual` 키워드를 사용하면 EF Core가 프록시(subclass)를 생성하여 해당 속성에 대한 접근을 가로채고, 

필요할 때 Lazy Loading을 수행할 수 있게 한다.

그러나 실제 Lazy Loading 기능을 활성화하려면 위에 언급된 조건들이 모두 충족되어야 한다.


virtual 키워드를 사용한 명시적 외래키 설정 예제

public class Blog // 부모엔티티
{ 
    public int BlogId { get; set; } 
    public string Name { get; set; } 

    public virtual ICollection<Post> Posts { get; set; } 
} 

public class Post // 자식엔티티
{ 
    public int PostId { get; set; } 
    public string Title { get; set; } 
    public string Content { get; set; } 

    public int BlogId { get; set; } 
    public virtual Blog Blog { get; set; } 
}

코드에 대한 설명

위의 코드에서 'Blog' 클래스는 'Post' 클래스를 참조하는 명시적 외래키 'Posts'를 가지고 있고,

'virtual' 키워드를 사용하고 있다.

이는 'Blog' 객체가 처음 로드되었을 때 'Posts' 속성이 로드되지 않지만 'Posts' 속성에 처음으로 접근했을 때

데이터베이스에서 'Posts'에 대한 데이터를 로드한다.

 

Lazy Loading의 장단점

Lazy Loading은 데이터베이스에서 필요한 데이터만 가져와 성능을 향상시키는 데 도움이 된다.

하지만 'Lazy Loading'은 예상치 못한 데이터베이스 쿼리를 발생시킬 수 있으므로 주의해서 사용해야 한다.

 

Lazy Loading 단점을 회피할 수 있는 방법

Lazy Loading 의 장단점에서 

Lazy Loading방식을 사용하면 예상치 못한 데이터베이스 쿼리를 발생시킬 수 있으므로 주의해서 사용해야 한다고 했다.

'Lazy Loading' 이란 객체가 실제 필요로 될 때 까지 그것의 로드를 연기하는 패턴이다.

EF에서의 'Lazy Loading' 은 'virtual' 키워드를 사용하는 네비게이션 속성에 적용된다.

즉, 관련 데이터가 직접 요청될 때까지 데이터베이스 쿼리가 실행되지 않는다.

이것이 성능 최적화에 유리한 경우도 있지만, 

예상치 못한 시점에서 데이터베이스 쿼리가 실행되어 성능 저하를 일으키는 경우도 있다.

 

Blog객체와 관련된 모든 Post 객체를 가져오는 예제

var blog = context.Blogs.Find(1);

foreach(var post in blog.Posts)
{
    Console.WriteLine(post.Title);
}

위의 코드에서,

context.Blogs.Find(1); 코드는 데이터베이스에서 Blog 객체를 가져온다.

하지만 이 시점에서 'Posts' 속성은 로드되지 않는다.

foreach 문에서 'Posts' 속성에 접근할 때, 'Lazy Loading'에 의해 'Posts'에 대한 데이터베이스 쿼리가 실행된다.

이처럼 Lazy Loading 이 적용된 네비게이션 속성에 접근하는 모든 곳에서 데이터베이스 쿼리가 발생할 수 있으므로

많은 양의 데이터에 대해 'Lazy Loading'을 사용하면

예상치 못한 시점에 많은 양의 데이터베이스 쿼리가 발생하여 성능 저하를 일으킬 수 있다.

Lazy Loading을 사용할 때 이런점을 고려해서 주의해야 한다.

 

Lazy Loading 회피 방법 1. 'Eager Loading'

'Eager Loading'을 사용하여

필요한 모든 관련 데이터를 처음 쿼리 시에 한 번에 로드하는 방법을 사용할 수 있다.

예를들어 EF에서는 Include() 메서드를 사용하여 'Eager Loading'을 수행한다.

 

Lazy Loading 회피 방법 2. 'Explicit Loading'

EF 에서 Load() 메서드를 이용하여 객체를 명시적으로 로드하는 방법도 제공한다.

이 메서드는 DBContext 의 DbCollectionEntry, DbReferenceEntry 에서 사용이 가능하다.

이것은 Eager Loading 과 Lazy Loading의 중간 형태이고

'Explicit Loading'이라고 불린다.

예시 : 특정 'Blog' 객체와 관련된 'Post' 객체들을 로드하고자 하여 Load() 메서드를 사용

var blog = context.Blogs.Find(1);

context.Entry(blog)
       .Collection(b => b.Posts)
       .Load();

context.Entry(blog)를 통해 'Blog' 객체의 엔트리를 가져온 후,

Collection(b => b.Posts)를 통해 'Posts' 컬렉션에 접근한다.

그 다음 Load() 메서드를 호출하여 'Posts' 컬렉션을 명시적으로 로드한다.

 

이 방법을 사용하면 'Lazy Loading'의 자동 로드 기능을 원하지 않는 경우에도 데이터베이스에서 필요한 시점에 데이터를 로드할 수 있다.

하지만 이러한 방식은 코드에서 직접 호출해야 하므로

복잡한 로직에서는 'Eager Loading'이나 'Lazy Loading'을 사용하는 것이 효율적일 수 있다.

 

의문점 1. Blog 클래스에서는 Posts 클래스만 작성되어있고 Id 를 명시적으로 작성하지 않은 이유

'Blog' 클래스에서 'Posts' 컬렉션만 가지는 이유는

이 클래스에서 'Post' 객체들에 대한 네비게이션 속성을 가지는 것이 목표이기 때문이다.

 

이는 'Blog' 객체가 연관된 모든 'Post' 객체를 로드할 수 있도록 해주는 것이다.

다른 한편으로 'Post' 클래스에서 'BlogId'와 'Blog' 를 모두 가지는 이유는 두가지 목적을 가지고 있다.

1. [외래키 속성] 'BlogId'는 외래키이다. 이는 데이터베이스 테이블 간의 관계를 정의하는 데 필요한 필드이다.

    'Post'가 어떤 'Blog'에 속하는지를 나타내는 역할을 한다.

2. 'Blog'는 네비게이션 속성이다. 

    이는 ORM(객체관계매핑) 시스템에서

    'Post'가 어떤 'Blog'에 속하는지를 코드 상에서 쉽게 알아낼 수 있게 해주는 역할을 한다.

    이를 통해 'Post' 객체가 속한 'Blog' 객체에 직접 접근할 수 있다.

 

'PostId'가 'Blog' 클래스에 없는 이유는, 

'Blog' 클래스에서 개별 'Post'의 ID 에 대한 정보는 필요하지 않기 때문이다.

'Blog' 클래스의 관점에서는 해당 블로그에 연결된 모든 'Post' 객체들만 필요하며,

각 'Post'의 ID는 'Post' 객체 자체에서 처리되는 정보이다.


 

의문점2. 네비게이션 객체 속성을 만들 때 인터페이스 객체를 사용하는 이유.

위의 예제를 기준으로 하여 네비게이션 객체 속성을 만들 때 인터페이스 객체를 사용하는 이유는

1. 범용성과 유연성

ICollection<T> 는 인터페이스이고, 

'List<T>', 'HashSet<T>', 'LinkedList<T>' 등 다양한 컬렉션 형식을 대표한다.

이것은 코드의 유연성을 높여준다.

ICollection<T> 타입의 속성을 사용하면 나중에 실제 컬렉션 타입을 변경하더라도

속성의 타입을 변경할 필요가 없다.

2. 필요한 연산 지원

ICollection<T> 인터페이스는 Add(), Clear(), Remove(), Count 등 많은 경우에 필요한 기본적인 컬렉션 연산을 제공한다.

대부분의 경우 이러한 연산이면 충분하고,

복잡한 연산을 필요로 하는 경우에는 특화된 컬렉션 인터페이스나 클래스를 사용해야 한다.

 

 

 

 

 


결론적으로 명시적 외래키 작성할 때 virtual 타입을 쓰는 이유는

Lazy Loading으로 데이터베이스에서 필요한 데이터만 가져와서 성능을 향상시키는 목적을 가지고 있다.