Tell, Don’t Ask 원칙

Tell, Don’t Ask 원칙이란?

Tell, Don’t Ask 원칙은 객체가 자신의 상태에 대해 물어보지 않고, 객체에게 그 역할을 “명령"하는 방식으로 설계를 유도하는 개념입니다. 이 원칙은 객체가 스스로 책임을 가지고 행동하게 함으로써, 객체 간 협력의 자연스러움을 높이고 응집도 있는 설계를 가능하게 합니다. 이는 객체가 자신의 데이터를 처리할 책임을 가지고 있다는 OOP의 기본 철학과 일맥상통합니다.

기본 개념

Tell, Don’t Ask 원칙은 간단히 말해, “객체의 상태를 묻는 대신, 객체에게 일을 시켜라"는 의미입니다. 객체지향 설계에서 흔히 발생하는 문제 중 하나는 객체의 상태를 외부에서 가져와 처리하는 방식인데, 이는 데이터와 메서드를 분리하여 객체의 역할을 축소하는 결과를 초래할 수 있습니다. 이 원칙은 그러한 문제를 방지하기 위해 객체에게 직접 일을 수행하도록 요청하는 방식을 권장합니다.

잘못된 설계 (Ask 방식):

public class Book
{
    public bool IsAvailable { get; set; }
}
public class LibraryService
{
    public void BorrowBook(User user, Book book)
    {
        // 책의 상태를 묻고, 그 상태에 따라 처리하는 방식
        if (book.IsAvailable)
        {
            book.IsAvailable = false; // 대출 처리
            Console.WriteLine("Book borrowed.");
        }
        else
        {
            Console.WriteLine("Book is not available.");
        }
    }
}
  • 위 코드는 Book 객체의 상태(IsAvailable)를 외부에서 물어보고(Ask), 그 상태에 따라 로직을 처리하는 방식입니다.
  • 이 방식은 Book 객체가 자신의 상태를 처리하지 않고, 외부에서 상태를 관리하는 문제를 일으킵니다.

올바른 설계 (Tell 방식):

public class Book
{
    private bool isAvailable = true;
    public bool Borrow()
    {
        if (!isAvailable)
        {
            return false; // 대출 실패
        }
        isAvailable = false;
        return true; // 대출 성공
    }
}
public class LibraryService
{
    public void BorrowBook(User user, Book book)
    {
        if (book.Borrow())
        {
            Console.WriteLine("Book borrowed.");
        }
        else
        {
            Console.WriteLine("Book is not available.");
        }
    }
}
  • Tell 방식에서는 Book 객체가 스스로 자신의 상태를 관리하며, Borrow() 메서드를 통해 대출 가능 여부를 처리합니다.
  • LibraryServiceBook에게 직접 대출을 요청하고, 그 결과를 받기만 합니다. 이를 통해 객체의 책임을 명확히 하며, 역할에 따른 응집도를 높일 수 있습니다.

장점

  • 객체의 책임 분리: Tell, Don’t Ask 원칙은 객체가 자신의 데이터를 처리할 책임을 가지게 하여, 코드의 응집성을 높이고, 유지보수를 쉽게 만듭니다.

  • 응집도 증가: 객체가 자신의 역할을 스스로 수행함으로써, 관련된 로직이 한 곳에 집중되고, 코드의 일관성을 유지할 수 있습니다.

  • 데이터 무결성 강화: 외부에서 객체의 상태를 직접적으로 변경하지 않음으로써, 객체 내부의 상태 변경에 대한 제어권을 해당 객체에 부여합니다.

유의사항

  • 객체가 너무 많은 책임을 가지게 되는 경우, 오히려 역할이 분산되어야 할 부분까지 하나의 객체에 몰리게 되는 문제가 발생할 수 있습니다. 따라서 Tell, Don’t Ask원칙 을 적용할 때는 단일 책임 원칙(SRP)과의 균형을 맞추는 것이 중요합니다.
  • 지나치게 내부 상태를 은닉하려다 객체 간의 협력이 부자연스러워질 수도 있습니다. 따라서 객체 간 상호작용의 맥락을 고려한 설계가 필요합니다.

맺음말

Tell, Don’t Ask 원칙은 객체지향 설계에서 객체 간의 자연스러운 협력과 책임 분배를 촉진하는 중요한 원칙입니다. 객체가 자신의 데이터를 스스로 처리하고 외부에서 이를 제어하지 않도록 함으로써, 코드의 응집성, 유지보수성, 그리고 확장성을 모두 향상시킬 수 있습니다. 실무에서는 이 원칙을 적절히 활용하여 객체 협력을 강화하고, 보다 유연하고 강력한 시스템을 설계할 수 있습니다.

심화 학습

비즈니스 로직에서의 응용

Tell, Don’t Ask 원칙은 복잡한 비즈니스 로직을 다루는 시스템에서 특히 유용하게 응용될 수 있습니다. 도서 관리 시스템에서 책 대출을 처리하는 로직을 보면, 책의 대출 상태를 외부에서 확인하고 처리하는 대신, Book 객체 자체가 대출 로직을 처리하도록 할 수 있습니다.

예시: 도서 대출 로직

public class Book
{
    private bool isBorrowed;
    public bool Borrow(User user)
    {
        if (isBorrowed)
        {
            return false; // 이미 대출된 경우
        }
        isBorrowed = true;
        user.AddBorrowedBook(this);
        return true;
    }
    public void Return() => isBorrowed = false;
    public bool IsBorrowed() => isBorrowed
}
  • 여기서 Book 객체는 책의 대출 요청을 스스로 처리합니다. 외부에서 책의 대출 상태를 확인하는 것이 아니라, Borrow 메서드를 호출하여 대출을 요청하면, 책 객체가 내부에서 상태 변화를 처리하고, 사용자의 대출 목록에 책을 추가합니다.
  • 이렇게 하면 외부에서 책 상태를 묻지 않고, 객체가 자체적으로 대출 로직을 처리하여 책임을 명확히 할당할 수 있습니다.

도메인 주도 설계와의 결합

도메인 주도 설계DDD에서 Tell, Don’t Ask 원칙을 적용하면, 애그리게이트 루트Aggregate Root가 자신이 관할하는 모든 엔터티와 값 객체의 상태를 책임지고 관리하는 구조를 만들 수 있습니다. 이는 객체 간 결합도를 줄이면서 도메인 로직을 강화하는 데 매우 유용합니다.

예시: 도서관 관리 시스템에서의 Tell, Don’t Ask

public class Library
{
    private List<Book> books = new List<Book>();
    public void AddBook(Book book)
    {
        books.Add(book);
    }
    public bool BorrowBook(User user, int bookId)
    {
        var book = books.FirstOrDefault(b => b.Id == bookId);
        if (book != null)
        {
            return book.Borrow(user);
        }
        return false; // 책이 존재하지 않음
    }
}
  • Library 클래스는 책의 대출을 처리할 때 책의 상태를 묻지 않고, Borrow 메서드를 호출하여 대출을 요청합니다.
  • 이렇게 하면 외부에서 책의 상태를 직접 확인하지 않고, 각 객체가 자신의 책임을 명확하게 수행하도록 할 수 있습니다.

원칙의 한계 및 극복 방안

Tell, Don’t Ask 원칙이 유용하지만, 모든 상황에 적용할 수 있는 것은 아닙니다. 객체 간의 책임이 지나치게 분산되거나, 지나친 캡슐화 로 인해 객체 간 상호작용이 복잡해질 수 있습니다. 이런 한계를 극복하기 위해 몇 가지 설계 기법과 패턴을 활용할 수 있습니다.

원칙의 한계

  • 책임 과부하: Tell 방식으로 설계하면 객체가 너무 많은 책임을 지게 되는 경우가 발생할 수 있습니다. 예를 들어, 비즈니스 로직과 데이터 관리가 하나의 객체에 몰릴 수 있습니다. 단일 책임 원칙SRP을 위반할 가능성을 높입니다.

  • 복잡성 증가: 객체가 지나치게 많은 역할을 가지게 되면, 전체 로직이 불필요하게 복잡해질 수 있습니다. 객체 간 협력이 필요 이상으로 증가하면서 설계가 비대해질 수 있습니다.

극복 방안

Command-Query 분리 원칙CQS과 같은 패턴을 활용하면, 객체가 특정 로직을 처리하는 동시에 다른 객체의 상태를 변경하지 않도록 설계를 개선할 수 있습니다. 또한, 도메인 서비스를 사용하여 비즈니스 로직을 여러 객체로 분리하여 객체 간 협력 구조를 간결하게 만들 수 있습니다.

public class Book
{
    private bool isBorrowed;
    public bool IsBorrowed() => isBorrowed
    public void Borrow(User user)
    {
        if (isBorrowed)
            throw new InvalidOperationException("이미 대출된 책입니다.");
        isBorrowed = true;
        user.AddBorrowedBook(this);
    }
    public void Return() => isBorrowed = false;
}
public class LibraryService
{
    public void BorrowBook(Book book, User user)
    {
        if (!book.IsBorrowed())
        {
            book.Borrow(user);
        }
    }
}
  • 이 예시에서 CQS 원칙을 적용하여, IsBorrowed()는 책의 상태를 조회하는 역할만 수행하고, 상태를 변경하는 로직은 Borrow 메서드로 분리되었습니다. LibraryService는 비즈니스 로직을 담당하며, Book 객체의 상태 변경을 조정합니다.
  • 이 방식은 Tell, Don’t Ask 원칙을 지키면서도, 비즈니스 로직의 복잡성을 낮추고 객체 간 협력을 보다 자연스럽게 만듭니다.

실무에서의 응용 전략

Tell, Don’t Ask 원칙은 다른 설계 패턴과 결합하여 실무에서 더욱 효과적으로 사용할 수 있습니다. 특히 Facade 패턴이나 서비스 패턴과 결합하면, 객체가 수행해야 하는 비즈니스 로직을 보다 명확하게 구분하고, 이를 호출하는 인터페이스를 간소화할 수 있습니다.

Facade 패턴과의 결합

public class LibraryFacade
{
    private LibraryService libraryService;
    private UserService userService;
    public LibraryFacade(LibraryService libraryService, UserService userService)
    {
        this.libraryService = libraryService;
        this.userService = userService;
    }
    public void BorrowBook(int userId, int bookId)
    {
        User user = userService.GetUserById(userId);
        Book book = libraryService.GetBookById(bookId);
        book.Borrow(user);
    }
}
  • LibraryFacadeLibraryServiceUserService를 사용하여 도서 대출 작업을 간소화합니다. 이는 Tell, Don’t Ask 원칙을 유지하면서, 여러 객체와의 복잡한 상호작용을 단일 인터페이스로 감추는 역할을 합니다.

유지보수와 확장성

Tell, Don’t Ask 원칙을 준수한 설계는 유지보수성과 확장성을 크게 향상시킵니다. 새로운 요구사항이 추가되더라도 객체가 각자의 책임을 명확히 가지고 있기 때문에, 특정 객체를 수정하더라도 전체 시스템에 미치는 영향을 최소화할 수 있습니다.