Unit of Work
Unit of Work 패턴이란?
Unit of Work
패턴은 여러 개의 데이터 변경 작업을 하나의 작업 단위로 묶어 관리하는 디자인 패턴입니다. 이 패턴은 데이터베이스와 상호작용할 때, 여러 개의 트랜잭션을 하나의 작업 단위로 처리하여, 작업이 모두 성공할 때만 커밋하고, 하나라도 실패하면 롤백할 수 있도록 해줍니다. 이를 통해 데이터의 일관성을 유지하고, 작업 간의 의존성을 관리할 수 있습니다.
Unit of Work 패턴의 특징
트랜잭션 관리Unit of Work
패턴은 여러 데이터 변경 작업을 하나의 트랜잭션으로 묶어 관리합니다. 즉, 여러 개의 Repository
가 작업을 수행하더라도, 한 번의 커밋으로 일괄 처리하여 데이터의 일관성을 보장합니다.
변경 추적
이 패턴은 엔터티의 상태 변경을 추적하여, 변경된 엔터티만 데이터베이스에 반영합니다. 이를 통해 불필요한 데이터베이스 작업을 줄이고 성능을 최적화할 수 있습니다.
작업 단위로의 묶음
여러 Repository
를 사용하여 데이터베이스 작업을 수행할 때, 하나의 작업 단위로 묶어서 처리합니다. 이는 각 Repository
의 작업을 개별적으로 처리하는 것보다 더 구조적이며, 오류 발생 시 모든 작업을 롤백할 수 있습니다.
Unit of Work 패턴 적용
Unit of Work 패턴의 필요성
여러 개의 Repository
를 사용하는 애플리케이션에서는 각각의 데이터 조작 작업을 개별적으로 처리할 경우, 데이터의 일관성이 깨질 위험이 있습니다. 이를 해결하기 위해 Unit of Work
패턴을 적용하면, 하나의 작업 단위로 여러 개의 Repository
작업을 처리할 수 있으며, 작업 중간에 오류가 발생하면 이전 작업을 모두 롤백할 수 있습니다.
잘못된 처리
public class BookService
{
private readonly IBookRepository _bookRepository;
private readonly IUserRepository _userRepository;
public BookService(IBookRepository bookRepository, IUserRepository userRepository)
{
_bookRepository = bookRepository;
_userRepository = userRepository;
}
public void BorrowBook(int userId, int bookId)
{
var user = _userRepository.GetUserById(userId);
var book = _bookRepository.GetBookById(bookId);
if (book.IsAvailable)
{
book.IsAvailable = false;
user.BorrowedBooks.Add(book);
_userRepository.UpdateUser(user);
_bookRepository.UpdateBook(book);
}
}
}
문제점
개별 트랜잭션 처리
위 코드에서는 UserRepository
와 BookRepository
의 각각의 작업이 별도로 이루어지고 있습니다. 만약 중간에 오류가 발생하면, 한쪽 작업은 성공하고 다른 쪽은 실패하는 일관성 문제가 발생할 수 있습니다.
트랜잭션의 부재
데이터베이스 작업을 개별적으로 처리하므로, 전체 작업에 대한 트랜잭션 관리가 이루어지지 않고, 오류 발생 시 롤백할 수 없습니다.
복잡한 로직
복잡한 비즈니스 로직을 처리할 때, Repository
를 통해 직접적으로 데이터를 수정하는 방식은 관리가 어렵고, 코드 중복이 발생할 수 있습니다.
Unit of Work 패턴 적용 예시
// Entity
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public bool IsAvailable { get; set; }
}
// Repository Interface
public interface IBookRepository
{
Book GetBookById(int id);
void UpdateBook(Book book);
}
// Unit of Work Interface
public interface IUnitOfWork : IDisposable
{
IBookRepository Books { get; }
IUserRepository Users { get; }
void Commit();
}
// Unit of Work 구현
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();
}
}
// Service Layer : Unit of Work 패턴 적용
public class BookService
{
private readonly IUnitOfWork _unitOfWork;
public BookService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public void BorrowBook(int userId, int bookId)
{
var user = _unitOfWork.Users.GetUserById(userId);
var book = _unitOfWork.Books.GetBookById(bookId);
if (book.IsAvailable)
{
book.IsAvailable = false;
user.BorrowedBooks.Add(book);
_unitOfWork.Users.UpdateUser(user);
_unitOfWork.Books.UpdateBook(book);
_unitOfWork.Commit();
}
}
}
개선사항
트랜잭션 관리 강화
잘못된 처리에서는 각 Repository
의 작업이 개별적으로 수행되어 일관성이 없었습니다. Unit of Work
패턴을 적용하면 모든 데이터 작업을 트랜잭션 단위로 묶어, 오류 발생 시 전체 작업을 롤백하여 데이터의 일관성을 유지할 수 있습니다.
중복 로직 제거
데이터베이스 작업이 Unit of Work
에 의해 관리되므로, 코드의 중복이 줄어들고 더 구조적인 방식으로 비즈니스 로직을 처리할 수 있습니다.
유지보수성 향상Unit of Work
패턴을 사용하면 서비스 레이어에서 트랜잭션 처리를 명확하게 관리할 수 있으므로, 코드 유지보수가 훨씬 용이해집니다.
Unit of Work 패턴 구성 요소
Entity (도메인 모델)Book
과 같은 도메인 엔터티는 비즈니스 로직과 데이터를 담고 있으며, Unit of Work
패턴에서 작업의 단위로 사용됩니다.
Repository Interface
각 Repository
는 데이터 접근 로직을 추상화하여, Unit of Work
와 함께 사용될 수 있습니다. 이를 통해 다양한 데이터 저장소와의 상호작용을 통일된 방식으로 처리할 수 있습니다.
Unit of Work InterfaceIUnitOfWork
는 여러 Repository
객체를 관리하며, 트랜잭션 단위로 데이터를 처리하는 메서드를 제공합니다. 이 인터페이스는 데이터베이스 작업을 한 번에 커밋하거나 롤백할 수 있도록 합니다.
Service Layer
서비스 레이어는 비즈니스 로직을 처리하며, Unit of Work
를 사용하여 여러 Repository
작업을 트랜잭션 단위로 묶어 관리합니다.
장점
트랜잭션 단위로 작업 처리Unit of Work
패턴은 여러 데이터베이스 작업을 하나의 트랜잭션으로 처리할 수 있어, 데이터의 일관성을 보장하고, 작업 간의 의존성을 명확히 관리할 수 있습니다.
데이터 변경 추적
변경된 엔터티만을 트래킹하여 데이터베이스에 반영하므로, 불필요한 데이터베이스 작업을 줄이고 성능을 최적화할 수 있습니다.
중복 코드 감소
여러 Repository
작업을 일관된 방식으로 처리하므로, 서비스 레이어에서 중복된 트랜잭션 처리 로직을 제거할 수 있습니다.
단점
복잡성 증가Unit of Work
패턴은 작은 프로젝트에서 과도한 복잡성을 초래할 수 있습니다. 여러 Repository
와 트랜잭션을 관리하는 로직이 필요 없는 단순한 애플리케이션에서는 오히려 불필요한 구조가 될 수 있습니다.
성능 이슈
모든 데이터 작업을 트랜잭션 단위로 처리하다 보면, 작업의 양이 많을 때 성능 저하가 발생할 수 있습니다. 트랜잭션을 잘못 관리하면 대규모 데이터 처리 시 문제가 될 수 있습니다.
맺음말
Unit of Work
패턴은 데이터베이스 작업의 일관성을 유지하고, 여러 Repository
가 상호작용할 때 데이터 변경을 추적하며 트랜잭션 단위로 관리할 수 있도록 도와줍니다. 이를 통해 대규모 시스템에서 데이터 처리의 안정성과 유지보수성을 크게 향상시킬 수 있습니다. 그러나 작은 프로젝트에서는 불필요한 복잡성을 초래할 수 있으므로, 사용 환경에 맞게 적절히 선택해야 합니다.
심화 학습
Unit of Work와 Repository
패턴의 결합
Unit of Work
패턴은 종종 Repository
패턴과 함께 사용됩니다. Repository
는 각 엔터티에 대한 데이터 접근을 추상화하고, Unit of Work
는 이러한 Repository
작업을 트랜잭션 단위로 묶어 처리합니다. 이 결합은 복잡한 비즈니스 로직에서 여러 Repository
작업이 한 번의 트랜잭션으로 일관성 있게 처리되도록 보장합니다.
예시: Repository
와 Unit of Work
패턴의 결합
// Repository Interface
public interface IBookRepository
{
Book GetBookById(int id);
void UpdateBook(Book book);
}
public interface IUserRepository
{
User GetUserById(int id);
void UpdateUser(User user);
}
// Unit of Work Interface
public interface IUnitOfWork : IDisposable
{
IBookRepository Books { get; }
IUserRepository Users { get; }
void Commit();
}
// Unit of Work Implementation
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();
}
}
// Service Layer
public class BorrowService
{
private readonly IUnitOfWork _unitOfWork;
public BorrowService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public void BorrowBook(int userId, int bookId)
{
var user = _unitOfWork.Users.GetUserById(userId);
var book = _unitOfWork.Books.GetBookById(bookId);
if (book.IsAvailable)
{
book.IsAvailable = false;
user.BorrowedBooks.Add(book);
_unitOfWork.Users.UpdateUser(user);
_unitOfWork.Books.UpdateBook(book);
_unitOfWork.Commit(); // 모든 작업을 트랜잭션 단위로 커밋
}
}
}
위 코드는 Repository
패턴을 통해 엔터티별 데이터 접근을 추상화하고, Unit of Work
가 이를 관리하여 여러 작업을 하나의 트랜잭션으로 처리하는 방식을 보여줍니다. 이로 인해 여러 Repository
가 관여하는 복잡한 비즈니스 로직에서도 데이터의 일관성을 보장할 수 있습니다.
Lazy Loading과 Eager Loading의 조합
Unit of Work
패턴을 사용할 때, Lazy Loading
과 Eager Loading
을 적절히 결합하여 성능을 최적화할 수 있습니다. Lazy Loading
은 필요할 때만 데이터를 불러오고, Eager Loading
은 한 번에 필요한 데이터를 모두 불러옵니다. 둘 사이의 균형을 맞추는 것이 중요합니다.
예시: Lazy Loading과 Eager Loading 결합
public class BookRepository : IBookRepository
{
private readonly DbContext _context;
public BookRepository(DbContext context)
{
_context = context;
}
public Book GetBookById(int id)
{
return _context.Books.Include(b => b.Author).FirstOrDefault(b => b.Id == id); // Eager Loading
}
public IEnumerable<Book> GetAllBooks()
{
return _context.Books.ToList(); // Lazy Loading
}
}
위 코드는 Lazy Loading
과 Eager Loading
을 적절히 사용하여 성능과 데이터 일관성을 유지합니다. 중요한 엔터티 간의 관계는 Eager Loading
으로 미리 불러오고, 필요 없는 데이터는 Lazy Loading
을 통해 불필요한 데이터베이스 호출을 줄입니다.
Unit of Work
패턴과 이벤트 소싱(Event Sourcing)
Unit of Work
패턴은 종종 Event Sourcing
과 결합되어 사용되기도 합니다. Event Sourcing
은 상태 변화 자체를 이벤트로 저장하여, 모든 상태 변화를 기록하고 필요할 때 복원하는 방식입니다. Unit of Work
는 이러한 상태 변화를 하나의 작업 단위로 묶어, 여러 상태 변화가 한 번에 처리되도록 관리할 수 있습니다.
예시: Unit of Work와 Event Sourcing 결합
public class Event
{
public int Id { get; set; }
public string EventType { get; set; }
public DateTime Timestamp { get; set; }
}
public class EventSourcingRepository
{
private readonly List<Event> _events = new List<Event>();
public void AddEvent(Event eventData)
{
_events.Add(eventData);
}
public IEnumerable<Event> GetEvents()
{
return _events;
}
}
public class UnitOfWorkWithEventSourcing : IUnitOfWork
{
private readonly DbContext _context;
private readonly EventSourcingRepository _eventRepository;
public UnitOfWorkWithEventSourcing(DbContext context, EventSourcingRepository eventRepository)
{
_context = context;
_eventRepository = eventRepository;
}
public IBookRepository Books { get; private set; }
public IUserRepository Users { get; private set; }
public void Commit()
{
_context.SaveChanges();
foreach (var eventData in _eventRepository.GetEvents())
{
// 이벤트 처리 로직
Console.WriteLine($"Processed event: {eventData.EventType}");
}
}
public void Dispose()
{
_context.Dispose();
}
}
위 코드는 Unit of Work
와 Event Sourcing
을 결합하여, 상태 변화를 이벤트로 기록하고 트랜잭션으로 처리하는 방식을 보여줍니다. 이는 복잡한 시스템에서 데이터의 일관성과 추적성을 높이는 데 매우 유용한 방식입니다.
테스트 주도 개발(TDD)에서의 Unit of Work 패턴
Unit of Work
패턴은 테스트 주도 개발(TDD)에서도 중요한 역할을 합니다. 이 패턴을 사용하면 데이터베이스에 의존하지 않고 비즈니스 로직을 테스트할 수 있으며, 트랜잭션 단위로 작업을 처리하므로 테스트의 일관성을 유지할 수 있습니다.
예시: Unit of Work를 사용한 TDD
public class MockUnitOfWork : IUnitOfWork
{
public IBookRepository Books { get; private set; }
public IUserRepository Users { get; private set; }
public MockUnitOfWork(IBookRepository bookRepository, IUserRepository userRepository)
{
Books = bookRepository;
Users = userRepository;
}
public void Commit() { /* 테스트에서는 커밋 로직을 생략 */ }
public void Dispose() { }
}
[TestMethod]
public void TestBorrowBook()
{
// Arrange
var mockBookRepo = new Mock<IBookRepository>();
var mockUserRepo = new Mock<IUserRepository>();
var mockUnitOfWork = new MockUnitOfWork(mockBookRepo.Object, mockUserRepo.Object);
var borrowService = new BorrowService(mockUnitOfWork);
mockBookRepo.Setup(b => b.GetBookById(It.IsAny<int>())).Returns(new Book { Id = 1, IsAvailable = true });
mockUserRepo.Setup(u => u.GetUserById(It.IsAny<int>())).Returns(new User { Id = 1, BorrowedBooks = new List<Book>() });
// Act
borrowService.BorrowBook(1, 1);
// Assert
mockBookRepo.Verify(b => b.UpdateBook(It.IsAny<Book>()), Times.Once);
mockUserRepo.Verify(u => u.UpdateUser(It.IsAny<User>()), Times.Once);
}
이 코드는 Unit of Work
패턴을 사용한 테스트 주도 개발 예시로, 실제 데이터베이스 대신 Mock
객체를 사용하여 비즈니스 로직을 검증합니다. 이를 통해 실제 데이터에 의존하지 않고도 중요한 로직을 테스트할 수 있습니다.