Identity Map
Identity Map 패턴이란?
Identity Map
패턴은 애플리케이션 내에서 동일한 데이터베이스 레코드에 대한 여러 개의 객체를 방지하고, 데이터를 메모리에 캐싱하는 방식으로 중복 객체 생성을 피하는 디자인 패턴입니다. 이 패턴은 객체의 ID를 기준으로 객체를 관리하며, 데이터베이스에서 조회한 엔터티를 메모리에 저장해 이후 동일한 요청이 들어오면 메모리에서 바로 객체를 반환합니다. 이를 통해 데이터베이스 접근 횟수를 줄이고 성능을 최적화할 수 있습니다.
Identity Map 패턴의 특징
객체의 일관성 유지Identity Map
패턴은 동일한 데이터베이스 레코드에 대해 여러 번 조회를 수행하더라도 같은 객체 인스턴스를 반환하여, 객체 상태의 일관성을 유지합니다.
메모리 캐싱을 통한 성능 향상
데이터베이스에서 조회된 객체를 메모리에 저장하고, 이후 동일한 데이터에 대한 요청이 있을 경우 메모리에서 바로 데이터를 반환하여 데이터베이스 쿼리 횟수를 줄입니다.
중복 객체 생성 방지
메모리 내에 객체를 캐싱함으로써 동일한 데이터를 여러 번 요청할 때마다 새로운 객체를 생성하는 문제를 방지합니다. 이를 통해 메모리 사용량을 최적화하고 애플리케이션 내 중복 객체 관리 문제를 줄일 수 있습니다.
Identity Map 패턴 적용
Identity Map 패턴의 필요성
복잡한 시스템에서 동일한 데이터에 대해 여러 번 조회하는 경우, 중복된 객체가 생성되거나 불필요하게 데이터베이스를 여러 번 호출할 수 있습니다. Identity Map
패턴을 적용하면 동일한 데이터에 대한 중복 객체 생성을 방지하고, 성능을 최적화할 수 있습니다.
잘못된 처리
public class BookService
{
private readonly IBookRepository _bookRepository;
public BookService(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public Book GetBookById(int id)
{
// 매번 데이터베이스에서 책을 조회함
return _bookRepository.GetBookById(id);
}
}
문제점
중복된 데이터베이스 호출
위 코드에서는 동일한 Book
객체를 여러 번 조회할 때마다 데이터베이스에 직접 접근합니다. 이는 성능 저하로 이어질 수 있습니다.
객체 일관성 문제
데이터베이스에서 동일한 레코드를 여러 번 조회할 때마다 새로운 객체가 생성되므로, 애플리케이션에서 여러 객체 인스턴스가 동시에 관리될 수 있습니다. 이는 데이터 일관성 문제를 초래할 수 있습니다.
Identity Map 패턴 적용 예시
// Identity Map 구현
public class IdentityMap<T> where T : class
{
private readonly Dictionary<int, T> _entities = new Dictionary<int, T>();
public T Get(int id)
{
_entities.TryGetValue(id, out var entity);
return entity;
}
public void Add(int id, T entity)
{
if (!_entities.ContainsKey(id))
{
_entities.Add(id, entity);
}
}
}
// Repository Interface
public interface IBookRepository
{
Book GetBookById(int id);
}
// Service Layer : Identity Map 패턴 적용
public class BookService
{
private readonly IBookRepository _bookRepository;
private readonly IdentityMap<Book> _identityMap = new IdentityMap<Book>();
public BookService(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public Book GetBookById(int id)
{
var book = _identityMap.Get(id);
if (book == null)
{
book = _bookRepository.GetBookById(id);
_identityMap.Add(id, book);
}
return book;
}
}
개선사항
불필요한 데이터베이스 호출 감소
기존의 잘못된 처리 방식에서는 동일한 데이터를 반복적으로 요청할 때마다 데이터베이스에 접근하였습니다. Identity Map
을 적용하면 객체를 한 번만 데이터베이스에서 가져온 후 메모리에 캐싱하여 이후 호출 시 데이터베이스에 접근할 필요가 없어, 불필요한 데이터베이스 호출을 줄일 수 있습니다.
일관된 객체 관리Identity Map
은 동일한 데이터를 요청할 때 동일한 객체 인스턴스를 반환하기 때문에, 여러 인스턴스가 불필요하게 생성되는 문제를 해결하고 애플리케이션 내에서 일관성 있는 객체 상태를 유지할 수 있습니다.
Identity Map 패턴 구성 요소
Identity Map
데이터베이스에서 조회한 객체를 메모리에 캐싱하여 관리하는 역할을 합니다. 이 클래스는 객체의 ID를 기준으로 객체를 조회하고 저장합니다.
Repository Interface
데이터베이스에 직접 접근하는 인터페이스로, 필요한 데이터를 조회하는 역할을 합니다. Identity Map
이 존재하지 않는 데이터를 요청할 때 사용됩니다.
Service Layer
비즈니스 로직을 처리하며, Identity Map
을 사용해 객체를 캐싱하고, 필요시 Repository
에서 데이터를 조회합니다.
장점
성능 최적화Identity Map
을 통해 데이터베이스 호출을 최소화함으로써 성능을 크게 향상시킬 수 있습니다. 특히 동일한 데이터를 여러 번 조회하는 애플리케이션에서 효과적입니다.
데이터 일관성 보장
동일한 객체에 대해 동일한 인스턴스를 반환하므로, 여러 객체 인스턴스 간의 상태 불일치 문제를 방지하고 데이터 일관성을 보장합니다.
중복 객체 관리 감소
메모리 내에서 객체를 캐싱하여 중복된 객체 생성을 방지하고, 애플리케이션 내의 메모리 사용량을 줄이는 데 기여합니다.
단점
메모리 사용량 증가
객체를 메모리에 저장하는 방식이므로, 캐싱되는 객체의 양이 많아질수록 메모리 사용량이 증가할 수 있습니다.
객체의 상태 관리 복잡성
캐싱된 객체의 상태를 적절히 관리하지 않으면, 캐싱된 데이터가 오래되어 일관성이 깨질 수 있습니다. 이를 방지하기 위한 갱신 로직이 필요합니다.
맺음말
Identity Map
패턴은 데이터베이스 접근을 최적화하고 객체 일관성을 유지하는 데 유용한 패턴입니다. 이를 통해 성능을 향상시키고, 동일한 데이터를 여러 번 조회하는 상황에서도 효율적으로 데이터를 관리할 수 있습니다. 하지만 메모리 사용량과 객체 상태 관리에 주의를 기울여야 하며, 적절한 캐싱 전략과 함께 사용해야 합니다.
심화 학습
Unit of Work
패턴과의 결합
Identity Map
패턴은 Unit of Work
패턴과 자주 결합되어 사용됩니다. Identity Map
은 엔터티의 중복 생성을 방지하고, Unit of Work
는 이러한 엔터티들의 상태를 추적하여 트랜잭션 단위로 처리합니다. 이 결합은 데이터 일관성을 유지하는 데 매우 유용하며, 메모리에서 관리되는 객체들의 상태를 데이터베이스에 반영할 때 유효합니다.
예시:
public class IdentityMap<T> where T : class
{
private readonly Dictionary<int, T> _entities = new Dictionary<int, T>();
public T Get(int id)
{
_entities.TryGetValue(id, out var entity);
return entity;
}
public void Add(int id, T entity)
{
if (!_entities.ContainsKey(id))
{
_entities.Add(id, entity);
}
}
}
public class UnitOfWork : IDisposable
{
private readonly DbContext _context;
private readonly IdentityMap<Book> _bookMap = new IdentityMap<Book>();
private readonly IdentityMap<User> _userMap = new IdentityMap<User>();
public Book GetBookById(int id)
{
var book = _bookMap.Get(id);
if (book == null)
{
book = _context.Books.Find(id);
_bookMap.Add(id, book);
}
return book;
}
public void Commit()
{
_context.SaveChanges();
}
public void Dispose()
{
_context.Dispose();
}
}
위 코드에서 Identity Map
과 Unit of Work
를 결합하여 중복 객체 생성을 방지하고, 트랜잭션 단위로 작업을 관리하는 방식을 보여줍니다. Unit of Work
는 트랜잭션을 처리하고, Identity Map
은 객체를 메모리에서 관리하며 중복 생성을 방지합니다.
Lazy Loading과 Eager Loading의 적용
Identity Map
패턴은 Lazy Loading 및 Eager Loading과 함께 사용되어 데이터베이스 조회 성능을 최적화할 수 있습니다. Lazy Loading은 필요한 시점에 데이터를 불러오고, Eager Loading은 처음부터 필요한 데이터를 모두 불러옵니다. Identity Map
은 이러한 로딩 전략을 보완하며, 메모리에 이미 로드된 데이터를 재사용하여 불필요한 데이터베이스 호출을 줄입니다.
예시: Lazy Loading과 Eager Loading의 결합
public class BookRepository
{
private readonly DbContext _context;
private readonly IdentityMap<Book> _identityMap = new IdentityMap<Book>();
public BookRepository(DbContext context)
{
_context = context;
}
public Book GetBookById(int id)
{
var book = _identityMap.Get(id);
if (book == null)
{
book = _context.Books.Include(b => b.Author).FirstOrDefault(b => b.Id == id); // Eager Loading
_identityMap.Add(id, book);
}
return book;
}
public IEnumerable<Book> GetAllBooks()
{
return _context.Books.ToList(); // Lazy Loading
}
}
이 코드는 Lazy Loading
과 Eager Loading
을 결합하여 성능을 최적화하는 방식입니다. Identity Map
은 메모리에 로드된 객체를 캐싱해 중복 조회를 방지하고, 불필요한 데이터베이스 호출을 줄입니다.
캐싱 전략과 성능 최적화
Identity Map
은 기본적으로 메모리 캐싱을 통해 성능을 최적화합니다. 그러나 캐싱된 데이터가 오래된 데이터가 될 위험이 있으므로 적절한 갱신 전략을 함께 사용해야 합니다. 캐시된 객체의 상태가 최신 데이터와 일치하는지 확인하기 위한 타이밍이나 조건을 관리하는 것이 중요합니다. 특히, 데이터 변경이 빈번한 환경에서는 캐시 무효화(invalidation) 또는 캐시 갱신 정책을 적용해야 합니다.
예시: 캐시 무효화 적용
public class BookRepository
{
private readonly DbContext _context;
private readonly IdentityMap<Book> _identityMap = new IdentityMap<Book>();
public Book GetBookById(int id)
{
var book = _identityMap.Get(id);
if (book == null)
{
book = _context.Books.Find(id);
_identityMap.Add(id, book);
}
return book;
}
public void UpdateBook(Book book)
{
_context.Books.Update(book);
_identityMap.Add(book.Id, book); // 캐시 갱신
}
public void RemoveBook(int id)
{
var book = _identityMap.Get(id);
if (book != null)
{
_context.Books.Remove(book);
_identityMap.Add(id, null); // 캐시 무효화
}
}
}
위 코드는 데이터베이스 업데이트 시 캐시된 객체도 갱신하고, 삭제 시 캐시에서 해당 객체를 무효화하여 데이터 일관성을 유지하는 예시입니다.
테스트 주도 개발(TDD)에서의 Identity Map 패턴
Identity Map
패턴을 사용하면 동일한 데이터에 대해 중복 객체 생성을 방지하므로, 테스트 환경에서도 일관된 데이터를 사용할 수 있습니다. 이는 Mocking과 함께 사용하여 테스트의 일관성을 높일 수 있으며, 캐싱된 객체를 통해 테스트 성능도 최적화할 수 있습니다.
예시: Identity Map을 사용한 TDD
[TestMethod]
public void TestGetBookById_IdentityMap()
{
var mockDbContext = new Mock<DbContext>();
var mockBook = new Book { Id = 1, Title = "Test Book" };
mockDbContext.Setup(db => db.Books.Find(It.IsAny<int>())).Returns(mockBook);
var bookRepo = new BookRepository(mockDbContext.Object);
// 첫 번째 호출은 DB에서 로드
var book1 = bookRepo.GetBookById(1);
Assert.AreEqual("Test Book", book1.Title);
// 두 번째 호출은 캐시에서 로드
var book2 = bookRepo.GetBookById(1);
Assert.AreSame(book1, book2); // 같은 객체를 반환해야 함
}
이 테스트는 Identity Map
패턴을 사용하여 첫 번째 호출에서 데이터를 로드하고, 두 번째 호출에서는 캐시된 객체를 반환하는지 확인하는 예시입니다. 이를 통해 성능을 최적화하고 테스트 일관성을 유지할 수 있습니다.
Identity Map 패턴의 확장성
Identity Map
패턴은 여러 데이터 소스를 처리하는 시스템에서 확장 가능합니다. 예를 들어, 파일 시스템이나 외부 API와 상호작용하는 경우에도 동일한 패턴을 적용하여 객체의 중복 생성을 방지하고, 성능을 최적화할 수 있습니다. 다양한 데이터 소스에서 객체를 조회할 때 Identity Map
을 사용하면 시스템 전체에서 일관성을 유지할 수 있습니다.