구성과 의존성 관리

구성과 의존성의 관계

구성은 한 객체가 다른 객체를 포함하여 그 기능을 사용하는 방식입니다. 예를 들어, 도서관(Library)이 책(Book) 객체를 포함하는 경우, Library는 여러 책을 관리하지만 책 객체와는 독립된 관계를 유지해야 합니다. 이처럼 구성된 객체들은 서로 독립적으로 변경될 수 있어야 하며, 이를 위해서는 의존성 관리가 필수적입니다. 객체 간 결합이 강해지면, 하나의 객체가 변경될 때 다른 객체에까지 영향을 미치게 되어 시스템의 유연성이 떨어집니다. 따라서 구성된 객체들의 의존성을 관리하여 결합도를 낮추고, 객체 간 관계를 느슨하게 유지하는 것이 중요합니다.

의존성 관리의 중요성

객체 간 의존성이 제대로 관리되지 않으면 다음과 같은 문제가 발생할 수 있습니다.

  • 결합도 증가: 객체들이 서로 밀접하게 의존하면, 하나의 변경 사항이 다른 객체에까지 영향을 미쳐 시스템이 깨지기 쉽습니다.
  • 유연성 부족: 의존성이 강하면 객체를 독립적으로 교체하거나 확장하기 어려워집니다.
  • 유지보수 어려움: 객체 간 관계가 복잡해질수록, 시스템이 복잡해지고 유지보수가 힘들어집니다.

의존성 관리 실패 예시

// Book 클래스: 도서의 기본 정보
public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
}
// BorrowService 클래스는 의존성을 직접 생성하고 관리함 (의존성 관리 실패)
public class BorrowService
{
    private List<Borrow> borrows = new List<Borrow>();
    public void AddBorrow(Book book, string borrower)
    {
        // Book 객체가 직접 생성되어 BorrowService 내부에서만 사용됨
        Book newBook = new Book { Title = book.Title, Author = book.Author };
        borrows.Add(new Borrow { BorrowedBook = newBook, BorrowedBy = borrower, BorrowDate = DateTime.Now });
        Console.WriteLine($"Borrow added for {newBook.Title} by {borrower}");
    }
    
    // 대출 목록 출력
    public void DisplayAllBorrows()
    {
        foreach (var borrow in borrows)
        {
            Console.WriteLine($"Borrowed Book: {borrow.BorrowedBook.Title}, Borrowed By: {borrow.BorrowedBy}");
        }
    }
}
// Borrow 클래스: 대출 정보를 관리
public class Borrow
{
    public Book BorrowedBook { get; set; }
    public string BorrowedBy { get; set; }
    public DateTime BorrowDate { get; set; }
}
// Library 클래스: BorrowService에 강하게 결합됨
public class Library
{
    private BorrowService borrowService = new BorrowService(); // 의존성 직접 생성
	// BorrowService에 직접 의존
    public void BorrowBook(Book book, string borrower) => borrowService.AddBorrow(book, borrower);
    public void ShowAllBorrows() => borrowService.DisplayAllBorrows();
}
// 사용 예시
Book book1 = new Book { Title = "The Great Gatsby", Author = "F. Scott Fitzgerald" };
Library library = new Library();  // Library는 BorrowService와 강하게 결합됨
library.BorrowBook(book1, "John Doe");
library.ShowAllBorrows();
  • 의존성 직접 생성: Library 클래스가 BorrowService 객체를 직접 생성합니다. 이로 인해 BorrowService의 구현을 변경하거나, 다른 대출 관리 서비스를 사용하려면 Library 클래스 자체를 수정해야 합니다.
  • 결합도 증가: LibraryBorrowService에 강하게 결합되어 있어, BorrowService의 변경이 Library에 직접적인 영향을 미치게 됩니다.
  • 유연성 부족: BorrowService를 교체하거나, 테스트 시 모의 객체Mock를 사용하기 어려워집니다.

의존성 관리 방법

의존성 주입

의존성 주입Dependency Injection, DI은 구성된 객체 간의 결합도를 낮추기 위한 대표적인 기법입니다. DI는 객체가 스스로 다른 객체를 생성하지 않고, 필요한 의존성을 외부에서 주입받는 방식을 의미합니다. 이를 통해 객체 간 결합도를 낮추고, 시스템을 더욱 유연하게 만들 수 있습니다.

의존성 주입의 주요 장점

  • 낮은 결합도: 객체 간 결합을 줄이고, 객체가 다른 객체의 구체적인 구현에 의존하지 않도록 합니다.
  • 테스트 가능성 증가: 의존성을 외부에서 주입받음으로써, 테스트 시 모의 객체Mock를 쉽게 주입할 수 있습니다.
  • 유연한 객체 교체: 시스템 변경 시 객체 간 결합이 낮아, 새로운 객체를 주입받아 사용할 수 있습니다.

예시: 도서관 시스템에서의 의존성 주입

// Book 클래스: 도서의 기본 정보
public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
    public void DisplayInfo() => Console.WriteLine($"{Title} by {Author}");
}
// Borrow 클래스: 대출 정보를 관리
public class Borrow
{
    public Book BorrowedBook { get; set; }
    public string BorrowedBy { get; set; }
    public DateTime BorrowDate { get; set; }
    public void DisplayBorrowInfo() => Console.WriteLine($"Borrowed Book: {BorrowedBook.Title}, Borrowed By: {BorrowedBy}, Borrow Date: {BorrowDate.ToShortDateString()}");
}
// BorrowService: 대출 관리 서비스를 구성
public class BorrowService
{
    private List<Borrow> borrows = new List<Borrow>();
    public void AddBorrow(Book book, string borrower)
    {
        borrows.Add(new Borrow { BorrowedBook = book, BorrowedBy = borrower, BorrowDate = DateTime.Now });
        Console.WriteLine($"Borrow added for {book.Title} by {borrower}");
    }
    public void DisplayAllBorrows()
    {
        foreach (var borrow in borrows)
        {
            borrow.DisplayBorrowInfo();
        }
    }
}
// Library 클래스: 의존성 주입을 통해 BorrowService를 주입받아 관리
public class Library
{
    private readonly BorrowService borrowService;
    // 의존성 주입: 외부에서 BorrowService를 주입받음
    public Library(BorrowService borrowService)
    {
        this.borrowService = borrowService;
    }
    public void BorrowBook(Book book, string borrower => borrowService.AddBorrow(book, borrower);
    public void ShowAllBorrows() => borrowService.DisplayAllBorrows();
}
// 사용 예시
BorrowService borrowService = new BorrowService();  // 의존성 주입을 위한 객체 생성
Library library = new Library(borrowService);       // 의존성 주입
Book book1 = new Book { Title = "The Great Gatsby", Author = "F. Scott Fitzgerald" };
library.BorrowBook(book1, "John Doe");
library.ShowAllBorrows();
  • 의존성 주입을 통해 Library 클래스는 BorrowService에 직접 의존하지 않고, 외부에서 BorrowService 객체를 주입받아 사용합니다.
  • 이는 객체 간 결합도를 낮추고, 객체 간 협력을 더 유연하게 만듭니다. 만약 BorrowService의 구현을 변경해야 할 경우, Library 클래스는 수정할 필요가 없으며 새로운 BorrowService 객체를 주입받아 사용하면 됩니다.

인터페이스 사용

인터페이스 기반 설계를 통해 객체 간 결합도를 낮출 수 있습니다. 객체는 구체적인 구현 대신 인터페이스에 의존함으로써, 언제든지 다른 구현체로 교체될 수 있습니다. 이는 의존성 주입과 함께 사용할 때 가장 효과적입니다.

public interface IBorrowService
{
    void AddBorrow(Book book, string borrower);
    void DisplayAllBorrows();
}
public class BorrowService : IBorrowService
{
    private List<Borrow> borrows = new List<Borrow>();
    public void AddBorrow(Book book, string borrower)
    {
        borrows.Add(new Borrow { BorrowedBook = book, BorrowedBy = borrower, BorrowDate = DateTime.Now });
    }
    public void DisplayAllBorrows()
    {
        foreach (var borrow in borrows)
        {
            borrow.DisplayBorrowInfo();
        }
    }
}
// Library 클래스는 IBorrowService 인터페이스를 주입받음
public class Library
{
    private readonly IBorrowService borrowService;
    public Library(IBorrowService borrowService)
    {
        this.borrowService = borrowService;
    }
    public void BorrowBook(Book book, string borrower) => borrowService.AddBorrow(book, borrower);
    public void ShowAllBorrows() => borrowService.DisplayAllBorrows();
}

의존성 주입 프레임워크 활용

실무에서는 의존성 주입 프레임워크(예: Autofac, Ninject, Unity)를 사용하여 객체 간 의존성을 자동으로 관리할 수 있습니다. 이를 통해 개발자는 의존성 관리에 신경 쓰지 않고, 객체 간 협력을 설정할 수 있습니다.

맺음말

구성과 의존성 관리는 객체지향 설계에서 객체 간 결합도를 낮추고, 시스템의 유연성과 유지보수성을 높이는 데 중요한 요소입니다. 의존성 주입은 구성된 객체 간 결합을 느슨하게 만들어, 객체 간 협력이 더 유연하게 이루어지도록 도와줍니다. 실무에서 이러한 기법들을 잘 활용하여, 시스템을 더 견고하고 유연하게 설계할 수 있습니다.