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);
        }
    }
}

문제점

개별 트랜잭션 처리
위 코드에서는 UserRepositoryBookRepository의 각각의 작업이 별도로 이루어지고 있습니다. 만약 중간에 오류가 발생하면, 한쪽 작업은 성공하고 다른 쪽은 실패하는 일관성 문제가 발생할 수 있습니다. 트랜잭션의 부재
데이터베이스 작업을 개별적으로 처리하므로, 전체 작업에 대한 트랜잭션 관리가 이루어지지 않고, 오류 발생 시 롤백할 수 없습니다. 복잡한 로직
복잡한 비즈니스 로직을 처리할 때, 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 Interface
IUnitOfWork는 여러 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 작업이 한 번의 트랜잭션으로 일관성 있게 처리되도록 보장합니다.

예시: RepositoryUnit 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 LoadingEager 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 LoadingEager 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 WorkEvent 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 객체를 사용하여 비즈니스 로직을 검증합니다. 이를 통해 실제 데이터에 의존하지 않고도 중요한 로직을 테스트할 수 있습니다.