.NET용 도메인 개체 캐싱 패턴

저자: 이크발 칸

추상

캐싱은 비용이 많이 드는 데이터베이스 이동을 줄여주기 때문에 애플리케이션 성능을 크게 향상시킵니다. 그러나 애플리케이션에서 캐싱을 사용하려면 캐싱할 대상과 캐싱 코드를 넣을 위치를 결정해야 합니다. 답은 간단합니다. 도메인 개체를 캐시하고 지속성 클래스 내부에 캐싱 코드를 넣습니다. 도메인 개체는 모든 응용 프로그램의 중심이며 핵심 데이터 및 비즈니스 유효성 검사 규칙을 나타냅니다. 또한 도메인 개체는 일부 읽기 전용 데이터를 유지할 수 있지만 대부분의 데이터는 트랜잭션이며 자주 변경됩니다. 따라서 데이터베이스에서 데이터가 변경되고 도메인 개체가 오래되어 데이터 무결성 문제가 발생하기 때문에 도메인 개체를 전체 응용 프로그램에 대해 "전역 변수"로 유지할 수 없습니다. 이를 위해 적절한 캐싱 솔루션을 사용해야 합니다. 그리고 옵션은 ASP.NET Cache, Microsoft Enterprise Library의 Caching Application Block 또는 다음과 같은 상용 솔루션입니다. NCache 에 Alachisoft . 개인적으로 ASP.NET 캐시를 사용하는 것은 좋지 않은 프레젠테이션 계층(ASP.NET 페이지)에서 캐시해야 하므로 사용하지 않는 것이 좋습니다. 응용 프로그램에 캐싱을 포함하는 가장 좋은 위치는 도메인 개체 지속성 클래스입니다. 이 기사에서는 .NET용 도메인 개체 지속성 패턴이라고 하는 이전 디자인 패턴을 확장하고 있습니다. 지능형 캐싱을 응용 프로그램에 통합하여 성능을 높이는 방법과 이를 수행하는 동안 염두에 두어야 할 고려 사항을 보여 드리겠습니다.

도메인 개체 캐싱 패턴은 도메인 개체 캐싱에 대한 솔루션을 제공하려고 합니다. 이 패턴의 도메인 개체는 종속성이 단방향이기 때문에 유지하는 클래스 또는 캐시되는지 여부를 인식하지 못합니다. 이것은 도메인 개체 디자인을 훨씬 간단하고 이해하기 쉽게 만듭니다. 또한 도메인 개체를 사용하는 다른 하위 시스템에서 캐싱 및 지속성 코드를 숨깁니다. 이것은 도메인 개체만 전달되는 분산 시스템에서도 작동합니다.

범위

도메인 개체, 도메인 개체 캐싱.

문제 정의

도메인 개체는 모든 애플리케이션의 백본을 형성합니다. 그들은 데이터베이스에서 데이터 모델과 이 데이터에 적용되는 비즈니스 규칙을 캡처합니다. 애플리케이션의 대부분의 하위 시스템이 이러한 공통 도메인 개체에 의존하는 것은 매우 일반적입니다. 그리고 일반적으로 응용 프로그램은 이러한 도메인 개체를 데이터베이스에 로드하거나 저장하는 데 대부분의 시간을 보냅니다. 이러한 개체의 실제 "처리 시간"은 특히 각 "사용자 요청"이 매우 짧은 N 계층 응용 프로그램의 경우 매우 짧습니다.

이것은 응용 프로그램의 성능이 이러한 도메인 개체를 응용 프로그램에서 얼마나 빨리 사용할 수 있는지에 따라 크게 좌우된다는 것을 의미합니다. 응용 프로그램이 데이터베이스를 많이 이동해야 하는 경우 일반적으로 성능이 나쁩니다. 그러나 응용 프로그램이 이러한 개체를 가까이에 캐시하면 성능이 크게 향상됩니다.

동시에 도메인 개체 캐싱 코드가 도메인 개체를 로드하거나 저장하는 사람과 상관없이 응용 프로그램이 자동으로 캐시와 상호 작용할 수 있도록 도메인 개체 캐싱 코드를 중앙에 보관하는 것이 매우 중요합니다. 또한 필요한 경우 쉽게 제거할 수 있도록 나머지 응용 프로그램에서 캐싱 코드를 숨겨야 합니다.

해법

위에서 설명한 것처럼 솔루션은 .NET용 도메인 개체 지속성 패턴이라는 기존 디자인 패턴의 확장입니다. 이 패턴은 도메인 객체를 지속성 코드 및 나머지 애플리케이션에서도 분리한다는 목표를 이미 달성했습니다. 이 이중 분리는 설계에 상당한 유연성을 제공합니다. 데이터가 관계형 데이터베이스에서 오는지 아니면 다른 소스(예: XML, 플랫 파일 또는 Active Directory/LDAP)에서 오는지 여부에 관계없이 도메인 개체와 나머지 응용 프로그램은 전혀 영향을 받지 않습니다.

따라서 캐싱 코드를 삽입하는 가장 좋은 위치는 지속성 클래스입니다. 이렇게 하면 응용 프로그램의 어느 부분이 도메인 개체에 대한 로드 또는 저장 호출을 실행하든 관계없이 캐싱이 먼저 적절하게 참조됩니다. 이것은 또한 애플리케이션의 나머지 부분에서 모든 캐싱 코드를 숨기고 선택하는 경우 다른 것으로 바꿀 수 있습니다.

도메인 및 지속성 클래스

이 샘플에서는 데이터베이스의 "Employees" 테이블에 매핑된 Northwind 데이터베이스의 Employee 클래스를 살펴보겠습니다.

// Domain object "Employee" that holds your data 
public class Employee {
    // Some of the private data members
    // ... 
    public Employee () { }

    // Properties for Employee object 
    public String EmployeeId {
        get { return _employeeId; }
        set { _employeeId = value; }
    }

    public String Title {
        get { return _title; }
        set { _title = value; }
    }

    public ArrayList Subordinates {
        get { return _subordinates; }
        set { _subordinates = value; }
    }
}
// Interface for the Employee persistence
public interface IEmployeeFactory {
    // Standard transactional methods for single-row operations 
    void Load (Employee emp);
    void Insert (Employee emp);
    void Update (Employee emp);
    void Delete (Employee emp);

    // Load the related Employees (Subordinates) for this Employee 
    void LoadSubordinates (Employee emp);

    // Query method to return a collection of Employee objects 
    ArrayList FindByTitle (String title);
}

// Implementation of Employee persistence 
public class EmployeeFactory : IEmployeeFactory {
    // all methods described in interface above are implemented here 
}

// A FactoryProvider to hide persistence implementation 
public class FactoryProvider {
    // To abstract away the actual factory implementation 
    public static IEmployeeFactory GetEmployeeFactory () { return new EmployeeFactory (); }
}

샘플 응용 프로그램

다음은 클라이언트 응용 프로그램이 이 코드를 사용하는 방법의 예입니다.

public class NorthwindApp
    {
        static void Main (string[] args) {
        Employee emp = new Employee();
        IEmployeeFactory iEmpFactory = FactoryProvider.GetEmployeeFactory();

        // Let's load an employee from Northwind database. 
        emp.EmployeeId = 2;
        iEmpFactory.load(emp);

        // Pass on the Employee object 
        HandleEmployee(emp);
        HandleSubordinates(emp.Subordinates);

        // empList is a collection of Employee objects  
        ArrayList empList = iEmpFactory.FindByTitle("Manager");
        }
    }

위의 코드는 도메인 개체 지속성 및 캐싱을 처리하기 위한 클래스의 전체 구조를 보여줍니다. 보시다시피 도메인과 지속성 클래스 사이에는 명확한 구분이 있습니다. 그리고 애플리케이션의 나머지 부분에서 지속성 구현을 숨길 수 있는 추가 FactoryProvider 클래스가 있습니다. 그러나 도메인 개체(이 경우 Employee)는 응용 프로그램 전체에서 이동합니다.

캐시 키 생성

대부분의 캐시 시스템은 문자열 기반 키를 제공합니다. 동시에 캐시하는 데이터는 다양한 클래스("고객", "직원", "주문" 등)로 구성됩니다. 이 상황에서 키에 유형 정보가 포함되어 있지 않으면 EmployeeId 1000이 OrderId 1000과 충돌할 수 있습니다. 따라서 일부 유형 정보도 키의 일부로 저장해야 합니다. 다음은 몇 가지 제안된 핵심 구조입니다. 동일한 원칙에 따라 자신을 구성할 수 있습니다.

  1. 개별 개체에 대한 키: 개별 개체만 저장하는 경우 다음과 같이 키를 구성할 수 있습니다.
    1. "고객:PK:1000". 이는 기본 키가 1000인 고객 개체를 의미합니다.
  2. 관련 개체에 대한 키: 각 개별 개체에 대해 쉽게 찾을 수 있도록 관련 개체를 보관할 수도 있습니다. 다음은 이에 대한 키입니다.
    1. "고객:PK:1000:REL:주문". 이는 기본 키가 1000인 고객에 대한 주문 컬렉션을 의미합니다.
  3. 쿼리 결과 키: 때때로 개체 컬렉션을 반환하는 쿼리를 실행합니다. 그리고 이러한 쿼리는 매번 다른 런타임 매개변수를 사용할 수도 있습니다. 다음 번에 쿼리를 실행할 필요가 없도록 이러한 쿼리 결과를 저장하려고 합니다. 여기 그 열쇠가 있습니다. 이러한 키에는 런타임 매개변수 값도 포함됩니다.
    1. "직원:QRY:FindByTitleAndAge:Manager:40". 이것은 두 개의 런타임 매개변수를 사용하는 "FindByTitleAndAge"라는 "Employees" 클래스의 쿼리를 나타냅니다. 첫 번째 매개변수는 "제목"이고 두 번째 매개변수는 "나이"입니다. 그리고 런타임 매개변수 값이 지정됩니다.

트랜잭션 작업의 캐싱

대부분의 트랜잭션 데이터에는 단일 행 작업(로드, 삽입, 업데이트 및 삭제)이 포함됩니다. 이러한 메서드는 모두 개체의 기본 키 값을 기반으로 하며 캐싱 코드를 시작하기에 이상적인 위치입니다. 각 방법을 처리하는 방법은 다음과 같습니다.

  1. 로드 방법: 먼저 캐시를 확인하십시오. 데이터가 발견되면 거기에서 가져옵니다. 그렇지 않으면 데이터베이스에서 로드한 다음 캐시에 넣습니다.
  2. 삽입 방법 : 데이터베이스에 행을 성공적으로 추가한 후 해당 개체도 캐시에 추가합니다.
  3. 업데이트 방법 : 데이터베이스에서 행을 성공적으로 업데이트한 후 캐시에서도 해당 개체를 업데이트합니다.
  4. 삭제 방법 : 데이터베이스에서 행을 성공적으로 제거한 후 캐시에서도 해당 개체를 제거합니다.

다음은 캐싱 로직이 포함된 샘플 Load 메서드입니다. 데이터베이스에서 단일 개체(단일 행)만 로드한다는 것을 기억하십시오.

// Check the cache before going to the database 
void Load(Employee emp)
    { 
        try{
            // Construct a cache-key to lookup in the cache first
            // The cache-key for the object will be like this: Employees:PK:1000stringobjKey = CacheUtil.GetObjectKey("Employee", emp.EmployeeId.ToString());

            objectobj = CacheUtil.Load(objKey);
            if(obj == null)
            {
                // item not found in the cache. Load from database and then store in the cache_LoadFromDb(emp);

                // For simplicity, let's assume this object does not depend on anything elseArrayListdependencyKeys = null;
                CacheItemRemovedCallbackonRemoved = null;

                CacheUtil.Store(objKey, emp, dependencyKeys, Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration, CacheItemPriority.Default, onRemoved );

                // Now, load all its related subordinatesLoadSubordinates(emp);
            }
            else {
                    emp.Copy((Employee)obj);
                }
        }
        catch(Exception e)
        {
            // Handle exceptions here
        }
    }

여기에서 몇 가지 사항에 유의하십시오.

  1. 제거된항목콜백: 지정된 항목이 캐시에서 제거될 때 애플리케이션이 비동기적으로 알림을 받을 수 있도록 하는 대리자입니다.
  2. 만료: 절대 시간 또는 유휴 시간 만료를 지정할 수 있습니다. 위에서 만료를 지정하지 않았지만 두 가지 유형의 만료를 지정할 수 있습니다. 하나는 고정 시간 만료(예: 지금부터 10분)이고 두 번째는 유휴 시간 만료(예: 항목이 2분 동안 유휴 상태인 경우)입니다.

캐싱 관계

도메인 개체는 일반적으로 관계형 데이터베이스에서 오는 관계형 데이터를 나타냅니다. 따라서 이들을 캐시할 때 이들의 관계를 염두에 두고 관련 개체도 캐시해야 합니다. 또한 개체와 모든 관련 개체 간에 "종속성"을 만들어야 합니다. 그 이유는 캐시에서 개체를 제거하는 경우 관련 개체도 모두 제거해야 데이터 무결성 문제가 발생하지 않기 때문입니다. 다음은 캐시에서 관계를 지정하는 방법의 코드 예입니다.

// LoadSubordinates method 
void LoadSubordinates (Employee emp) {
    try {
        // Construct a cache-key to lookup related items in the cache first
        // The cache-key for related collection will be like
        this : Employees : PK : 1000 : REL : Subordinates
        string relKey = CacheUtil.GetRelationKey ("Employees",
            "Subordinates", emp.EmployeeId.ToString ());
        string employeeKey = CacheUtil.GetObjectKey ("Employee",
            emp.EmployeeId.ToString ());

        object obj = CacheUtil.Load (relKey);
        if (obj == null) {
            // Subordinates not found in the cache. Load from
            database and then store in the cache
            _LoadSubordinatesFromDb (emp);

            ArrayList subordList = emp.Subordinates;

            // Result is a collection of Employee. Let's store
            each Employee separately in
                // the cache and then store the collection also but
                with a dependency on all the
            // individual Employee objects. Then, if any Employee
            is removed, the collection will also be
            // Count + 1 is so we can also put a dependency on
            the Supervisor
            ArrayList dependencyKeys = new ArrayList (subordList.Count + 1);
            for (int index = 0; index, subordList.Count; index++) {
                string objKey = CacheUtil.GetObjectKey ("Employee",
                    subordList[index].EmployeeId.ToString ());
                CacheUtil.Store (objKey, subordList[index], null,
                    Cache.NoAbsoluteExpiration,
                    Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
                dependencyKeys[index] = objKey;
            }
            dependencyKeys[subordList.Count] = employeeKey;
            CacheItemRemovedCallback onRemoved = null;
            CacheUtil.Store (relKey, subordinateList,
                dependencyKeys, Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration, CacheItemPriority.Default,
                onRemoved);
        } else {
            // Subordinates already in the cache. Let's get them
            emp.Subordinates = (ArrayList) obj;
        }
    }
    catch (Exception e) {
        // Handle exceptions here 
    }
}

위의 예에서 컬렉션이 데이터베이스에서 반환되고 컬렉션 내부의 각 개체가 캐시에 개별적으로 저장됨을 알 수 있습니다. 그런 다음 컬렉션은 단일 항목으로 캐시되지만 컬렉션의 모든 개별 개체에 대한 캐시 종속성이 있습니다. 즉, 캐시에서 개별 개체가 업데이트되거나 제거되면 컬렉션이 캐시에 의해 자동으로 제거됩니다. 이를 통해 캐싱 컬렉션에서 데이터 무결성을 유지할 수 있습니다.

또한 위의 예에서 컬렉션에 관련 개체가 포함된 "기본 개체"에 대한 캐시 종속성이 있음을 알 수 있습니다. 이 종속성은 캐시에서 기본 개체가 제거되거나 업데이트되는 경우 데이터 무결성을 유지하기 위해 컬렉션이 제거됨을 의미합니다.

쿼리 메서드의 캐싱

쿼리 메서드는 지정된 검색 기준에 따라 개체 컬렉션을 반환합니다. 런타임 매개변수를 사용하거나 사용하지 않을 수 있습니다. 이 예에서는 "제목"을 매개변수로 사용하는 FindByTitle이 있습니다. 다음은 쿼리 메서드에 캐싱을 포함하는 방법의 예입니다.

// Query method to return a collection 
ArrayList FindByTitle (String title) {
    try {
        // Construct a cache-key to lookup items in the cache first
        // The cache-key for the query will be like this:
        Employees : PK : 1000 : QRY : FindByTitle : Manager
        string queryKey = CacheUtil.GetQueryKey ("Employees",
            "Query", title);

        object obj = CacheUtil.Load (queryKey);
        if (obj == null) {
            // No items found in the cache. Load from database
            and then store in the cache
            ArrayList empList = _FindByTitleFromDb (title);

            // Result is a collection of Employee. Let's store
            each Employee separately in
                // the cache and then store the collection also
                but with a dependency on all the
            // individual Employee objects. Then, if any Employee
            is removed, the collection will also be
            ArrayList dependencyKeys = new ArrayList (empList.Count);
            for (int index = 0; index, empList.Count; index++) {
                string objKey = CacheUtil.GetObjectKey ("Employee",
                    empList[index].EmployeeId.ToString ());

                CacheUtil.Store (objKey, empList[index], null, Cache.NoAbsoluteExpiration,
                    Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
                dependencyKeys[index] = objKey;
            }

            CacheItemRemovedCallback onRemoved = null;
            CacheUtil.Store (queryKey, empList, dependencyKeys,
                Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration, CacheItemPriority.Default, onRemoved);
        } else {
            // Query results already in the cache. Let's get them 
            return (ArrayList) obj;
        }
    }
    catch (Exception e) {
        // Handle exceptions here 
    }
}

위의 예에서 관계 메서드와 마찬가지로 컬렉션이 데이터베이스에서 반환되고 컬렉션 내부의 각 개체가 캐시에 개별적으로 저장됨을 알 수 있습니다. 그런 다음 컬렉션은 단일 항목으로 캐시되지만 컬렉션의 모든 개별 개체에 대한 캐시 종속성이 있습니다. 즉, 캐시에서 개별 개체가 업데이트되거나 제거되면 컬렉션이 캐시에 의해 자동으로 제거됩니다. 이를 통해 캐싱 컬렉션에서 데이터 무결성을 유지할 수 있습니다.

서버 팜의 애플리케이션

위의 패턴은 단일 서버 또는 서버 팜 배포 환경 모두에서 작동합니다. 변경해야 하는 유일한 것은 기본 캐싱 솔루션입니다. 대부분의 캐싱 솔루션은 단일 서버 환경을 위한 것입니다(예: ASP.NET Cache 및 Caching Application Block). 그러나 다음과 같은 상용 솔루션이 있습니다. Alachisoft NCache (http : // www.alachisoft.com) 서버 팜 구성에서 작동하는 분산 캐시를 제공합니다. 이러한 방식으로 애플리케이션은 팜에 있는 모든 서버의 캐시를 사용할 수 있으며 모든 캐시 업데이트는 전체 서버 팜에 즉시 전파됩니다.

결론

도메인 개체 캐싱 패턴을 사용하여 캐싱 코드를 지속성 클래스에 포함하는 방법을 보여주었습니다. 그리고 캐싱과 관련하여 로드, 쿼리 및 관계의 가장 일반적으로 사용되는 상황을 다루었습니다. 이것은 애플리케이션에서 캐싱을 어떻게 사용해야 하는지 결정하기 위한 좋은 출발점이 될 것입니다.


저자: 이크발 칸(Iqbal Khan)이 근무하는 곳 Alachisoft, .NET 및 Java 분산 캐싱, O/R 매핑 및 SharePoint 스토리지 최적화 솔루션을 제공하는 선도적인 소프트웨어 회사입니다. 당신은 그에게 연락할 수 있습니다 이크발@alachisoft.COM.

© 저작권 Alachisoft 2002 - . 판권 소유. NCache 는 Diyatech Corp.의 등록상표입니다.