Repository 패턴
Repository 패턴이란?
Repository 패턴은 데이터베이스와의 상호작용을 캡슐화하고, 애플리케이션의 비즈니스 로직이 데이터 저장소의 구체적인 구현 세부사항에 의존하지 않도록 만드는 디자인 패턴입니다. 이 패턴은 데이터 액세스 로직을 추상화하여 데이터베이스와의 상호작용을 인터페이스로 감싸서, 더 일관된 방식으로 데이터를 처리할 수 있게 합니다. 또한 데이터베이스와의 상호작용을 모듈화하고 유지보수성을 높이는 데 중요한 역할을 합니다.
Repository 패턴의 특징
Repository 패턴은 데이터 접근 로직을 비즈니스 로직에서 분리하는 패턴으로, 다양한 데이터 저장소와의 상호작용을 일관된 방식으로 처리할 수 있도록 설계되었습니다. 다음은 주요 특징입니다:
데이터 접근 추상화
Repository 패턴은 데이터베이스나 다른 저장소와의 상호작용을 추상화하여, 비즈니스 로직이 특정 데이터 소스의 구현 세부 사항에 의존하지 않도록 합니다. 즉, 실제 저장소의 구조나 접근 방법이 변경되더라도, 인터페이스를 통해 상호작용함으로써 코드의 수정 범위를 최소화할 수 있습니다.
통일된 데이터 접근 방식
데이터 저장소에 대한 CRUD(Create, Read, Update, Delete) 작업을 표준화하여 코드의 일관성을 유지합니다. 이를 통해 모든 데이터 접근 작업이 통일된 방식으로 이루어져, 유지보수성과 가독성이 향상됩니다.
느슨한 결합
Repository 패턴은 인터페이스를 통해 구현체를 주입받는 방식으로 동작하여, 비즈니스 로직과 데이터 접근 로직 간의 결합도를 낮춥니다. 이를 통해 쉽게 Mock 객체를 주입하여 단위 테스트를 수행할 수 있고, 다른 저장소로 교체하는 작업도 간편해집니다.
단일 책임 원칙 준수
Repository는 데이터베이스와의 상호작용을 담당하고, 비즈니스 로직은 이와 별개로 처리됩니다. 각 클래스가 하나의 책임에 집중할 수 있으므로, 단일 책임 원칙(SRP)을 자연스럽게 준수할 수 있습니다.
Repository 패턴 적용
Repository 패턴의 필요성
비즈니스 로직이 데이터베이스와 밀접하게 결합되어 있으면, 데이터베이스 스키마 변경이나 다른 저장소로의 전환 시 많은 코드 수정이 필요합니다. Repository 패턴은 이러한 문제를 해결하기 위해 데이터 접근을 추상화하여, 비즈니스 로직과 데이터 접근 로직을 분리합니다. 이를 통해 애플리케이션은 더 유연해지고, 유지보수성이 높아집니다.
잘못된 처리
public class BookService
{
private List<Book> _books = new List<Book>();
public Book GetBookById(int id) => _books.FirstOrDefault(b => b.Id == id);
public void AddBook(Book book) => _books.Add(book);
public void RemoveBook(int id)
{
var book = GetBookById(id);
if (book != null)
{
_books.Remove(book);
}
}
}
문제점
비즈니스 로직과 데이터 접근 로직의 결합BookService
클래스는 데이터 접근 로직과 비즈니스 로직을 모두 포함하고 있어 단일 책임 원칙을 위반합니다. 이로 인해, 데이터베이스가 변경되면 비즈니스 로직을 수정해야 하고, 코드의 유지보수성이 떨어집니다.
재사용성의 문제
데이터 접근 로직이 서비스 레이어에 포함되어 있기 때문에 다른 서비스에서 Book
엔터티에 접근하려면 동일한 코드를 복사하거나 중복된 코드를 작성해야 합니다.
테스트 용이성 부족
데이터 접근이 직접적으로 서비스 클래스에 구현되어 있어, 비즈니스 로직만을 독립적으로 테스트하기 어렵습니다. 실제 데이터베이스와의 상호작용이 없으면 코드 검증이 불가능해집니다.
Repository 패턴 적용 예시
Repository 패턴을 적용하여 데이터 접근 로직을 추상화하고, 비즈니스 로직에서 독립적으로 처리하는 방법입니다.
// Entity
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
}
// Repository Interface
public interface IBookRepository
{
Book GetBookById(int id);
IEnumerable<Book> GetAllBooks();
void AddBook(Book book);
void RemoveBook(int id);
}
// Repository 구현
public class BookRepository : IBookRepository
{
private readonly List<Book> _books = new List<Book>();
public Book GetBookById(int id) => _books.FirstOrDefault(b => b.Id == id);
public IEnumerable<Book> GetAllBooks() => _books;
public void AddBook(Book book) => _books.Add(book);
public void RemoveBook(int id)
{
var book = GetBookById(id);
if (book != null)
{
_books.Remove(book);
}
}
}
// Service Layer : BookService에서 Repository 사용
public class BookService
{
private readonly IBookRepository _bookRepository;
public BookService(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public Book GetBook(int id) => _bookRepository.GetBookById(id);
public void AddBook(Book book) => _bookRepository.AddBook(book);
public void RemoveBook(int id) => _bookRepository.RemoveBook(id);
}
개선내용
데이터베이스 전환의 유연성
잘못된 처리에서는 데이터 접근 로직이 서비스 레이어에 하드코딩되어 있어, 데이터 저장소를 변경할 때마다 수정이 필요했습니다. Repository 패턴을 적용하면, IBookRepository
인터페이스를 통해 데이터 접근 로직이 추상화되므로, 데이터베이스 변경 시 서비스 코드를 수정할 필요가 없어 유연성이 높아집니다.
단일 책임 원칙 준수 잘못된 코드에서는 하나의 클래스가 비즈니스 로직과 데이터 접근 로직을 동시에 처리하고 있었습니다. Repository 패턴을 적용하면 데이터 접근은 Repository에서, 비즈니스 로직은 서비스 레이어에서 처리하도록 분리됩니다. 이를 통해 각 클래스가 단일 책임을 가지게 되어 유지보수성과 가독성이 향상됩니다. 테스트 용이성 잘못된 코드에서는 데이터 접근과 비즈니스 로직이 결합되어 있어 테스트가 어려웠습니다. Repository 패턴을 적용한 후에는 데이터베이스와 상관없이 인터페이스를 통해 데이터를 Mocking할 수 있어, 비즈니스 로직을 독립적으로 테스트할 수 있습니다. 이를 통해 테스트 코드가 더 간결하고 유지보수 가능해집니다.
Repository 패턴 구성 요소
Entity (도메인 모델)
Book
클래스와 같은 도메인 엔터티는 비즈니스 로직과 데이터를 담고 있으며, Repository 패턴에서 주요 데이터 단위로 사용됩니다.
Repository Interface
IBookRepository
와 같은 인터페이스는 데이터 접근의 규격을 정의합니다. 이를 통해 구현체가 변경되더라도 비즈니스 로직은 영향을 받지 않습니다.
Repository 구현체
BookRepository
클래스는 실제 데이터 소스에 접근하는 구현체입니다. 데이터베이스, 메모리, 파일 시스템 등 여러 데이터 소스에 대한 구체적인 구현을 여기서 담당합니다.
Service Layer
BookService
는 비즈니스 로직을 담당하며, Repository를 통해 데이터를 조회하거나 조작합니다.
장점
데이터 접근 로직의 중앙화 모든 데이터베이스 접근 로직을 하나의 Repository 클래스로 집중시킴으로써 유지보수성을 높이고, 코드의 중복을 줄일 수 있습니다. 이를 통해 데이터베이스 변경이나 스키마 수정 시 여러 클래스에서 수정이 발생하는 문제를 방지할 수 있습니다. 단위 테스트 용이성 Repository를 인터페이스로 분리하여, 실제 데이터베이스와 상호작용하지 않고도 쉽게 모킹(Mock)하여 단위 테스트를 수행할 수 있습니다. 이 방식은 데이터베이스 의존성을 배제하고 비즈니스 로직을 독립적으로 검증하는 데 유용합니다. 비즈니스 로직 분리 데이터베이스와의 직접적인 상호작용이 비즈니스 로직에서 분리되므로, 비즈니스 로직이 깔끔해지고 데이터 저장소의 변화에 유연하게 대처할 수 있습니다.
단점
추가적인 복잡성 작은 프로젝트에서는 굳이 Repository 패턴을 사용하지 않아도 될 수 있습니다. 데이터베이스 접근이 매우 단순하다면 이 패턴이 오히려 불필요한 복잡성을 초래할 수 있습니다. 데이터베이스에 특화된 기능의 손실 데이터베이스가 제공하는 고유한 쿼리나 기능을 추상화하려다 보면, 성능 저하나 특정 기능을 충분히 활용하지 못할 수 있습니다.
맺음말
Repository 패턴은 데이터 액세스 로직을 추상화하고, 비즈니스 로직과의 결합도를 낮추는 데 매우 유용한 패턴입니다. 특히, 대규모 시스템에서 데이터 저장소가 다양해지거나 변경될 가능성이 있는 경우 유지보수성과 확장성을 높이는 데 크게 기여할 수 있습니다.
심화 학습
Unit of Work
패턴 결합
Unit of Work
패턴은 Repository 패턴과 자주 결합되어 사용됩니다. 이 패턴은 여러 Repository가 데이터베이스와 상호작용할 때, 작업을 트랜잭션 단위로 묶어주는 역할을 합니다. 즉, 여러 데이터베이스 작업을 한 번에 수행하고, 성공하면 커밋(commit)하고, 실패하면 롤백(rollback)할 수 있습니다.
예시:
public interface IUnitOfWork : IDisposable
{
IBookRepository Books { get; }
IUserRepository Users { get; }
void Commit();
}
public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _context;
public IBookRepository Books { get; private set; }
public IUserRepository Users { get; private set; }
public UnitOfWork(DbContext context, IBookRepository bookRepository, IUserRepository userRepository)
{
_context = context;
Books = bookRepository;
Users = userRepository;
}
public void Commit()
{
_context.SaveChanges();
}
public void Dispose()
{
_context.Dispose();
}
}
위 코드에서는 UnitOfWork
클래스가 여러 Repository
객체를 관리하고, Commit()
메서드를 통해 모든 변경 사항을 한 번에 커밋합니다. 이를 통해 데이터의 일관성과 트랜잭션을 쉽게 관리할 수 있습니다.
쿼리 객체(Query Object) 패턴 통합
복잡한 쿼리 로직을 Repository 패턴에 직접 포함시키는 것은 바람직하지 않습니다. 대신 쿼리 객체(Query Object) 패턴을 사용하여 복잡한 쿼리를 분리할 수 있습니다. 쿼리 객체는 명시적인 쿼리를 만들어 데이터베이스에 요청하고, 그 결과를 반환하는 역할을 합니다.
예시
public class BookQueryObject
{
private readonly IQueryable<Book> _query;
public BookQueryObject(IQueryable<Book> query)
{
_query = query;
}
public IQueryable<Book> FilterByAuthor(string author)
{
return _query.Where(b => b.Author == author);
}
public IQueryable<Book> SortByTitle()
{
return _query.OrderBy(b => b.Title);
}
}
public class BookRepository : IBookRepository
{
private readonly DbContext _context;
public BookRepository(DbContext context)
{
_context = context;
}
public IQueryable<Book> GetBooksQuery()
{
return _context.Set<Book>();
}
public IEnumerable<Book> GetBooksByAuthor(string author)
{
var queryObject = new BookQueryObject(GetBooksQuery());
return queryObject.FilterByAuthor(author).ToList();
}
}
위 코드는 BookQueryObject
클래스를 통해 복잡한 쿼리를 관리하며, BookRepository
는 GetBooksByAuthor
메서드를 통해 쿼리 객체를 사용해 필터링된 결과를 반환합니다. 이는 복잡한 쿼리 로직을 분리해 코드의 유지보수성과 가독성을 높입니다.
테스트를 위한 Mock Repository
Repository 패턴의 큰 장점 중 하나는 인터페이스로 정의된 데이터를 쉽게 Mocking하여 단위 테스트를 작성할 수 있다는 점입니다. 이를 통해 실제 데이터베이스와 상호작용하지 않고도 다양한 시나리오에 대한 테스트를 수행할 수 있습니다.
예시: Mock Repository를 이용한 단위 테스트
public class MockBookRepository : IBookRepository
{
private readonly List<Book> _books = new List<Book>
{
new Book { Id = 1, Title = "Clean Code", Author = "Robert C. Martin" },
new Book { Id = 2, Title = "Domain-Driven Design", Author = "Eric Evans" }
};
public Book GetBookById(int id)
{
return _books.FirstOrDefault(b => b.Id == id);
}
public IEnumerable<Book> GetAllBooks()
{
return _books;
}
public void AddBook(Book book)
{
_books.Add(book);
}
public void RemoveBook(int id)
{
var book = GetBookById(id);
if (book != null)
{
_books.Remove(book);
}
}
}
위 예시에서는 MockBookRepository
를 통해 실제 데이터베이스와의 상호작용 없이 Repository의 동작을 테스트할 수 있습니다. 이렇게 함으로써 Repository가 다양한 상황에서 올바르게 동작하는지 확인할 수 있습니다.
성능 최적화: Lazy Loading과 Eager Loading
Repository 패턴에서는 Lazy Loading과 Eager Loading을 사용하여 성능을 최적화할 수 있습니다. Lazy Loading은 필요한 시점에 데이터를 불러오고, Eager Loading은 데이터를 한 번에 가져오는 방식입니다. 각 방식은 상황에 따라 적절히 선택해야 하며, 성능과 데이터 일관성 측면에서 중요한 역할을 합니다.
예시: Eager Loading 적용
public class BookRepository : IBookRepository
{
private readonly DbContext _context;
public BookRepository(DbContext context)
{
_context = context;
}
public IEnumerable<Book> GetBooksWithAuthorDetails()
{
return _context.Books.Include(b => b.Author).ToList(); // Eager Loading
}
}
Include
메서드를 사용하여 관련 데이터를 한 번에 로드하는 Eager Loading을 구현합니다. 이 방식은 여러 테이블 간의 조인을 필요로 하는 경우 성능을 최적화하는 데 유용합니다.
Repository 패턴의 확장성
Repository 패턴은 확장 가능한 구조를 제공하므로, 다중 데이터 소스를 다룰 때 유용하게 사용할 수 있습니다. 예를 들어, 파일 시스템, API, NoSQL 데이터베이스 등 다양한 데이터 소스와도 쉽게 연동할 수 있으며, 각각의 데이터 소스에 맞는 Repository를 구현하여 관리할 수 있습니다.