Specification Pattern

꽤 괜찮은 글이 있어 공유합니다.

Avoid Proliferating DbContext or IQueryable in .NET Apps | Blog (ardalis.com)

요지

  1. DbContex, DbSet, IQueryable 을 데이터 레이어 외부로 노출시키지 마라.
  2. Specification 객체로 쿼리를 한정하라.

DbContex, DbSet, IQueryable 을 외부로 노출시키지 마라.

대표적인 이유는 IQueryable 이 IEnumerable 을 파생하고 있어, 사용 코드에서 IEnumerable로 취급할 수 있기 때문입니다.

foreach 돌릴 때마다 쿼리가 전송되기 하고, 사용자 코드 단에서 IEnumerable 의 확장 메서드를 정의했을 경우, IQueryable 을 쿼리로 변환하는 쿼리 컨버터가 이를 해석할 수 없기 때문에 예외가 발생합니다.
이런 경우, 컴파일 타임에 문제가 드러나지도 않습니다.

Specification 객체로 쿼리를 한정하라.

아래 코드처럼 위험한 코드도 없죠.

var allUsers = context.Users.ToList();

이러한 성능 위험을 초래하는 코드 뿐만 아니라, 코드가 제한 없이 데이터에 접근하는 것 자체가 매우 위험하다고 생각해왔습니다. 이는 웹 API 같은 도메인 외부 노출 인터페이스는 당연하고, 도메인 내부의 코드도 마찬가지입니다.

이를 위해, 데이터 서비스가 DbContext 를 감싸고, 필터링된 인터페이스만 노출하는 구조를 채택할 수 있습니다.

sealed class UserService
{
   public const int MaxUserRetrieveCount = 50;
   private IQueryable<User> _users;
   private int _retrieved;
   ...
   public List<User> Next(int count) 
   {
      if (count < 1)
         return new();

      if (MaxUserRetrieveCount < count)
         count = MaxUserRetrieveCount;

      var skip = _retrieved;
      _retrieved += count;
      return _users.Skip(skip).Take(count).ToList();
   }
   ...
}

저는 이 방법을 주로 사용해 왔는데 가장 큰 문제는 유지 보수입니다.

필터링 케이스가 변경/추가될 때마다 UserService 의 코드가 변경되어야 해서 그렇습니다.
특히 여러 곳에서 호출되고 있는 케이스를 수정하려면 수정해야 할 코드의 양이 쉽게 많아집니다.

Specification 패턴은 단일 필터링 케이스를 캡슐화하기 때문에 유지보수 측면에서 매우 뛰어난 것 같습니다.

6 Likes

직접 쿼리를 제한하고 Interface로 접근한다는것이 원론적으로 맞긴 하나 ?
사실 실무에서 그러기가 만만치 않습니다.
제한할 쿼리를 만든다는것은 이미 업무 로직을 거의 끝냈다는 애기일수도 있고
팀단위로 개발할때 작은 변경에도 담당자가 수정해줘야 할수도 있고
그리고 디버깅시 한단계 더 거쳐야 한다는것일수도 있고
뭐 단점만 나열하면 이런데 맞는 방향은 맞으나 귀찮다라는 단점같네요

2 Likes

사실 말씀하신 것들 모두 인터페이스를 사용할 경우 해결되는 문제이기도 합니다. OOP에서 주장하는게 그거죠.

다만 문제는 모든 팀원들의 OOP 수준이 같지 않다는 점 일 것 같습니다.

인터페이스의 개념이나 용도를 잘 모르니까 그저 인터페이스를 클래스 말고 또 뭔가 만들어줘야하는 귀찮은 것.

이라고 생각하기 때문일 것 같네요. 제가 그럤던 것처럼…

3 Likes

아 물론 여기서 언급하시는 인터페이스는 코딩의 인터페이스는 아니지만 결국 인터페이스를 통해 접근만 하면 사용하는 측에서는 인터페이스에만 접근하고 실질적인 구현은닉을 함으로써 개발자가 코드에 집중한다는 맥락에서 말씀드렸습니다.

오해는 없으시기를…!

2 Likes

어떤 레이어를 별도로 업무 분장했으면, 그 레이어에 관한 코드는 그 담당자에게 요청하는 것이 당연한 것 아닌지요?

2 Likes

이 패턴은 인터페이스라는 메시지를 효율적으로 확장하는 방법을 제시하는 것으로 이해했습니다.

메시지 갯수를 확장(메서드 종류를 늘림)할 것보다는, 메시지는 단일화하고, 매개 변수를 통해 행위를 확장하는 것이죠.

4 Likes

네네 제가 코드의 interface를 예시를 들어서 오히려 제가 말하려 했던 의도가 헷갈리실 것 같습니다.

인터페이스라는 것의 본질을 말하고 싶었습니다.

저에게 인터페이스는 콘센트 같은 것이라서 사람이 전기를 사용하고 싶을 때 콘센트 뒷편에서 어떻게 작동하는지는 몰라도 되고 그저 220V 전원 선을 꽂기만 하면 된다 라는 명확한 행동만 있게 해줄 수 있는 고마운 개념입니다.

제가 아는 선에선 모든 인터페이스라 불리는 것들이 다 이런 개념을 갖고 있는 것 같습니다.

3 Likes