Data Access Object(DAO) 패턴

Data Access Object (DAO) 패턴이란?

Data Access Object (DAO) 패턴은 데이터베이스나 파일 시스템 같은 데이터 저장소와 상호작용하는 로직을 캡슐화하여, 비즈니스 로직과 데이터 접근 로직을 분리하는 디자인 패턴입니다. DAO 패턴을 사용하면 애플리케이션의 비즈니스 로직이 데이터 저장소의 구체적인 구현 세부사항에 의존하지 않도록 설계할 수 있습니다.

DAO 패턴의 특징

데이터 접근 추상화
DAO는 데이터베이스와의 상호작용을 추상화하여, 구체적인 저장소의 변경이 비즈니스 로직에 영향을 미치지 않도록 합니다. 데이터베이스 독립성
DAO 패턴을 통해 데이터베이스 관련 로직이 캡슐화되므로, 데이터베이스를 변경하거나 다른 종류의 데이터 저장소를 도입할 때 코드 수정이 최소화됩니다. 재사용성
DAO 패턴을 통해 데이터 접근 로직을 모듈화하여, 동일한 데이터 저장소에 접근하는 로직을 여러 곳에서 재사용할 수 있습니다. 느슨한 결합
DAO는 인터페이스를 통해 구현되므로, 비즈니스 로직과 데이터 접근 로직 간의 결합도가 낮아집니다. 이를 통해 유지보수성과 테스트 용이성이 높아집니다.

DAO 패턴 적용

DAO 패턴의 필요성

DAO 패턴은 데이터베이스와의 상호작용을 추상화하여 데이터 접근 로직이 비즈니스 로직과 분리되도록 설계합니다. 이를 통해 데이터베이스 변경이나 데이터 저장소 전환 시 최소한의 코드 수정만으로 대응할 수 있으며, 유지보수성과 확장성을 높일 수 있습니다.

잘못된 처리 예시

public class BookService
{
    private readonly string _connectionString = "DatabaseConnection";
    public Book GetBookById(int id)
    {
        using (var connection = new SqlConnection(_connectionString))
        {
            connection.Open();
            var command = new SqlCommand($"SELECT * FROM Books WHERE Id = {id}", connection);
            var reader = command.ExecuteReader();
            if (reader.Read())
            {
                return new Book
                {
                    Id = (int)reader["Id"],
                    Title = (string)reader["Title"],
                    Author = (string)reader["Author"]
                };
            }
            return null;
        }
    }
}

문제점

데이터 접근과 비즈니스 로직의 결합
데이터베이스와 직접적인 상호작용이 서비스 계층에 포함되어 있어 단일 책임 원칙을 위반하고, 유지보수성이 떨어집니다. 확장성 부족
데이터베이스 변경 시 서비스 레이어 코드 수정이 필요하며, 새로운 데이터 저장소로 확장하는 것이 어렵습니다. 테스트 용이성 부족
데이터 접근이 직접적으로 서비스 클래스에 구현되어 있어, 데이터베이스 의존성을 제거한 단위 테스트 작성이 어렵습니다.

DAO 패턴 적용 예시

DAO 패턴을 적용하여 데이터 접근 로직을 서비스 로직에서 분리하는 방법입니다.

// Entity
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
}
// DAO 인터페이스
public interface IBookDao
{
    Book GetBookById(int id);
    IEnumerable<Book> GetAllBooks();
    void AddBook(Book book);
    void RemoveBook(int id);
}
// DAO 구현
public class BookDao : IBookDao
{
    private readonly string _connectionString = "DatabaseConnection";
    public Book GetBookById(int id)
    {
        using (var connection = new SqlConnection(_connectionString))
        {
            connection.Open();
            var command = new SqlCommand($"SELECT * FROM Books WHERE Id = {id}", connection);
            var reader = command.ExecuteReader();
            if (reader.Read())
            {
                return new Book
                {
                    Id = (int)reader["Id"],
                    Title = (string)reader["Title"],
                    Author = (string)reader["Author"]
                };
            }
            return null;
        }
    }
    public IEnumerable<Book> GetAllBooks()
    {
        var books = new List<Book>();
        using (var connection = new SqlConnection(_connectionString))
        {
            connection.Open();
            var command = new SqlCommand("SELECT * FROM Books", connection);
            var reader = command.ExecuteReader();
            while (reader.Read())
            {
                books.Add(new Book
                {
                    Id = (int)reader["Id"],
                    Title = (string)reader["Title"],
                    Author = (string)reader["Author"]
                });
            }
        }
        return books;
    }
    public void AddBook(Book book)
    {
        using (var connection = new SqlConnection(_connectionString))
        {
            connection.Open();
            var command = new SqlCommand("INSERT INTO Books (Title, Author) VALUES (@Title, @Author)", connection);
            command.Parameters.AddWithValue("@Title", book.Title);
            command.Parameters.AddWithValue("@Author", book.Author);
            command.ExecuteNonQuery();
        }
    }
    public void RemoveBook(int id)
    {
        using (var connection = new SqlConnection(_connectionString))
        {
            connection.Open();
            var command = new SqlCommand("DELETE FROM Books WHERE Id = @Id", connection);
            command.Parameters.AddWithValue("@Id", id);
            command.ExecuteNonQuery();
        }
    }
}
// 서비스 레이어
public class BookService
{
    private readonly IBookDao _bookDao;
    public BookService(IBookDao bookDao)
    {
        _bookDao = bookDao;
    }
    public Book GetBook(int id) => _bookDao.GetBookById(id);
    public void AddBook(Book book) => _bookDao.AddBook(book);
    public void RemoveBook(int id) => _bookDao.RemoveBook(id);
}

개선사항

데이터 접근 로직 분리 DAO 패턴을 적용하면 데이터베이스와의 상호작용을 별도의 DAO 클래스로 분리하여, 비즈니스 로직은 데이터 접근 방식에 영향을 받지 않도록 할 수 있습니다. 단일 책임 원칙 준수 데이터 접근 로직은 DAO에서, 비즈니스 로직은 서비스 레이어에서 처리하도록 책임을 분리하여 코드의 가독성과 유지보수성을 향상시킵니다. 테스트 용이성 향상 DAO 인터페이스를 통해 데이터베이스를 추상화하여, 실제 데이터베이스와 상호작용하지 않고도 Mock 객체를 사용해 단위 테스트를 작성할 수 있습니다.

DAO 패턴 구성 요소

Entity (도메인 모델)
Book과 같은 엔티티는 DAO 패턴에서 데이터베이스 테이블과 매핑되는 객체로 사용됩니다. 이 객체는 비즈니스 로직과 데이터 저장소 간의 데이터 단위로, 데이터베이스의 레코드를 객체로 나타냅니다. DAO 인터페이스
DAO 인터페이스는 데이터 접근에 필요한 메서드들의 규격을 정의합니다. 예를 들어 IBookDao 인터페이스는 GetBookById, GetAllBooks, AddBook, RemoveBook과 같은 메서드를 포함하여 데이터베이스 접근 로직의 기본 구조를 설정합니다. 이 인터페이스는 데이터 접근 로직의 추상화 계층으로, 구현체가 변경되더라도 비즈니스 로직은 동일한 방식으로 데이터에 접근할 수 있습니다. DAO 구현체
DAO 인터페이스를 구현한 클래스는 실제 데이터베이스와 상호작용하는 로직을 담당합니다. 예를 들어 BookDao 클래스는 SQL 쿼리나 데이터베이스 연결을 관리하며, 데이터베이스와의 상호작용을 구체적으로 처리합니다. Service Layer
서비스 레이어는 비즈니스 로직을 담당하며, DAO 인터페이스를 통해 데이터에 접근합니다. 서비스 레이어는 데이터베이스의 구체적인 구현 세부사항을 알 필요 없이, DAO가 제공하는 메서드들을 사용하여 데이터를 처리합니다. 데이터베이스 연결
DAO 구현체는 데이터베이스와의 연결을 관리합니다. 이 연결은 일반적으로 데이터베이스 커넥션 객체(SqlConnection 등)를 사용하여 설정되며, DAO 구현체는 데이터베이스의 세부사항을 처리하는 책임을 가집니다.

DAO 패턴의 장점

유지보수성 향상
데이터 접근 로직이 중앙화되어 있어, 데이터 저장소 변경 시 DAO 클래스만 수정하면 되므로 유지보수성이 높아집니다. 테스트 가능성
DAO를 인터페이스로 정의하여, 테스트 시에는 데이터베이스 의존성을 제거한 Mock 객체를 주입하여 테스트할 수 있습니다. 확장성
DAO 패턴을 통해 데이터 저장소가 변경되거나 여러 저장소로 확장할 때, 비즈니스 로직은 수정 없이 DAO 클래스만 교체하면 됩니다.

DAO 패턴의 단점

추상화로 인한 성능 저하 가능성
데이터 접근 로직을 추상화하는 과정에서 특정 데이터베이스의 고유한 기능을 활용하지 못해 성능이 저하될 수 있습니다. 추가적인 코드 작성
작은 프로젝트에서는 DAO 패턴이 오히려 불필요한 복잡성을 초래할 수 있습니다. 특히, 데이터베이스 접근이 매우 단순할 때는 별도의 DAO 클래스를 작성하는 것이 비효율적일 수 있습니다.

맺음말

DAO 패턴은 데이터 접근 로직을 비즈니스 로직에서 분리하여 코드의 유지보수성, 확장성, 테스트 용이성을 높이는 데 유용한 패턴입니다. 그러나, 프로젝트의 복잡성에 따라 패턴 적용의 이점을 잘 고려해 결정해야 합니다.

심화 학습

DAO 패턴과 데이터베이스 커넥션 풀링

데이터베이스와의 연결을 매번 새로 설정하는 것은 성능 저하를 초래할 수 있습니다. DAO 패턴을 사용할 때는 데이터베이스 커넥션 풀링을 적용하여, 매번 새로운 연결을 생성하는 대신 이미 생성된 커넥션을 재사용함으로써 성능을 최적화할 수 있습니다.

예시: 커넥션 풀링 적용

public class ConnectionManager
{
    private static SqlConnection _connection;
    public static SqlConnection GetConnection()
    {
        if (_connection == null || _connection.State == ConnectionState.Closed)
        {
            _connection = new SqlConnection("YourConnectionStringHere");
            _connection.Open();
        }
        return _connection;
    }
}
public class BookDao : IBookDao
{
    public Book GetBookById(int id)
    {
        using (var connection = ConnectionManager.GetConnection())
        {
            var command = new SqlCommand("SELECT * FROM Books WHERE Id = @id", connection);
            command.Parameters.AddWithValue("@id", id);
            using (var reader = command.ExecuteReader())
            {
                if (reader.Read())
                {
                    return new Book
                    {
                        Id = (int)reader["Id"],
                        Title = (string)reader["Title"],
                        Author = (string)reader["Author"]
                    };
                }
            }
        }
        return null;
    }
}

이 코드에서 ConnectionManager는 커넥션을 관리하고 재사용하여 성능을 향상시킵니다. DAO 구현체는 커넥션 풀링을 통해 효율적으로 데이터베이스와 상호작용합니다.

DAO와 트랜잭션 관리

DAO 패턴을 사용할 때는 트랜잭션 관리가 중요한 요소입니다. 데이터베이스 트랜잭션을 올바르게 처리하지 않으면 데이터 일관성을 유지하기 어려워질 수 있습니다. 트랜잭션을 통해 여러 데이터베이스 작업을 하나의 단위로 묶고, 성공 시 커밋하고 실패 시 롤백하는 방식으로 안정성을 확보할 수 있습니다.

예시: 트랜잭션 관리

public class BookDao : IBookDao
{
    public void UpdateBooks(List<Book> books)
    {
        using (var connection = ConnectionManager.GetConnection())
        {
            using (var transaction = connection.BeginTransaction())
            {
                try
                {
                    foreach (var book in books)
                    {
                        var command = new SqlCommand("UPDATE Books SET Title = @title, Author = @author WHERE Id = @id", connection, transaction);
                        command.Parameters.AddWithValue("@title", book.Title);
                        command.Parameters.AddWithValue("@id", book.Id);
                        command.Parameters.AddWithValue("@author", book.Author);
                        command.ExecuteNonQuery();
                    }
                    transaction.Commit();
                }
                catch
                {
                    transaction.Rollback();
                    throw;
                }
            }
        }
    }
}

위 예시에서는 UpdateBooks 메서드가 여러 책 데이터를 수정하는데, 이 작업이 트랜잭션 내에서 실행됩니다. 모든 작업이 성공하면 커밋하고, 오류가 발생하면 롤백하여 데이터의 일관성을 유지합니다.

DAO와 캐싱

DAO 패턴에서 데이터베이스 접근을 최소화하고 성능을 향상시키기 위한 방법으로 캐싱을 활용할 수 있습니다. 캐싱을 통해 자주 접근하는 데이터를 메모리에 저장하고, 필요할 때마다 데이터베이스 대신 캐시에서 데이터를 가져오면 성능을 크게 개선할 수 있습니다.

예시: 캐싱 적용

public class CachedBookDao : IBookDao
{
    private readonly IBookDao _bookDao;
    private static Dictionary<int, Book> _cache = new Dictionary<int, Book>();
    public CachedBookDao(IBookDao bookDao)
    {
        _bookDao = bookDao;
    }
    public Book GetBookById(int id)
    {
        if (_cache.ContainsKey(id))
        {
            return _cache[id];
        }
        var book = _bookDao.GetBookById(id);
        if (book != null)
        {
            _cache[id] = book;
        }
        return book;
    }
    public IEnumerable<Book> GetAllBooks()
    {
        return _bookDao.GetAllBooks();
    }
    public void AddBook(Book book)
    {
        _bookDao.AddBook(book);
        _cache[book.Id] = book;
    }
    public void RemoveBook(int id)
    {
        _bookDao.RemoveBook(id);
        _cache.Remove(id);
    }
}

CachedBookDao는 기존의 BookDao를 감싸서 캐시를 적용한 구현체입니다. GetBookById 메서드에서 데이터를 캐시에서 먼저 조회한 후, 없으면 데이터베이스에서 가져와 캐시에 저장합니다. 이 방식을 통해 자주 조회되는 데이터에 대해 성능을 개선할 수 있습니다.

DAO와 비동기 프로그래밍

DAO 패턴에 비동기 프로그래밍을 적용하면, 데이터베이스 호출과 같은 I/O 작업이 오래 걸리는 작업을 비동기적으로 처리할 수 있습니다. 이를 통해 애플리케이션의 응답성을 높이고, 대규모 시스템에서 성능을 향상시킬 수 있습니다.

예시: 비동기 DAO 구현

public class BookDao : IBookDao
{
    public async Task<Book> GetBookByIdAsync(int id)
    {
        using (var connection = ConnectionManager.GetConnection())
        {
            var command = new SqlCommand("SELECT * FROM Books WHERE Id = @id", connection);
            command.Parameters.AddWithValue("@id", id);
            using (var reader = await command.ExecuteReaderAsync())
            {
                if (await reader.ReadAsync())
                {
                    return new Book
                    {
                        Id = (int)reader["Id"],
                        Title = (string)reader["Title"],
                        Author = (string)reader["Author"]
                    };
                }
            }
        }
        return null;
    }
}

위 예시에서는 비동기 메서드 GetBookByIdAsync를 통해 데이터베이스 조회 작업을 비동기적으로 처리합니다. 이를 통해 데이터베이스 조회가 완료될 때까지 기다리는 동안 다른 작업을 처리할 수 있어 시스템의 효율성을 높일 수 있습니다.

DAO 패턴과 ORM (객체-관계 매핑) 도구의 통합

DAO 패턴은 ORM 도구와 통합하여 사용할 수 있습니다. 대표적인 ORM 도구로는 Entity Framework가 있으며, 이를 통해 데이터베이스와의 상호작용을 더욱 추상화하고 복잡한 SQL을 직접 작성하지 않고도 데이터를 처리할 수 있습니다. ORM 도구는 SQL 생성, 트랜잭션 관리, 데이터 매핑 등을 자동으로 처리하여 개발자가 비즈니스 로직에만 집중할 수 있게 도와줍니다.

예시: Entity Framework와 DAO 패턴의 결합

public class BookDao : IBookDao
{
    private readonly DbContext _context;
    public BookDao(DbContext context)
    {
        _context = context;
    }
    public Book GetBookById(int id)
    {
        return _context.Books.Find(id);
    }
    public IEnumerable<Book> GetAllBooks()
    {
        return _context.Books.ToList();
    }
    public void AddBook(Book book)
    {
        _context.Books.Add(book);
        _context.SaveChanges();
    }
    public void RemoveBook(int id)
    {
        var book = _context.Books.Find(id);
        if (book != null)
        {
            _context.Books.Remove(book);
            _context.SaveChanges();
        }
    }
}

이 예시는 Entity Framework와 DAO 패턴을 결합하여 데이터를 관리하는 방법을 보여줍니다. Entity Framework는 데이터베이스와의 상호작용을 자동화하여, DAO 패턴을 더욱 간결하고 효율적으로 만듭니다.

DAO 패턴의 성능 최적화 전략

DAO 패턴을 적용할 때 성능 최적화는 중요한 요소입니다. 대규모 데이터 처리나 빈번한 데이터베이스 호출이 있는 애플리케이션에서는 성능을 최적화하기 위해 다양한 전략을 사용할 수 있습니다. Lazy Loading과 Eager Loading 필요한 시점에만 데이터를 가져오는 Lazy Loading과, 한 번에 필요한 데이터를 전부 가져오는 Eager Loading을 상황에 맞게 선택합니다. 쿼리 최적화 복잡한 쿼리를 최적화하여 데이터베이스의 부하를 줄이고, 적절한 인덱스를 사용하여 성능을 향상시킵니다. 캐싱 자주 사용되는 데이터를 캐싱하여 데이터베이스 호출을 최소화하고, 성능을 높입니다.