본문 바로가기

디자인패턴

의존 관계 제어

의존이란?

어떤 객체가 다른 객체를 참조하는 것.

여러가지 의존 관계가 있을 수 있는데, 다음은 그의 일부를 설명한다.

  1. 참조를 통해 발생하는 의존 관계
  2. public class ObjectA { private ObjectB objectB; }
  3. 일반화를 통한 의존 관계
     public interface IUserRepository
     {
         User Find(UserId id);
     }
    
     public class UserRepository: IUserRepository
     {
         public User Find(UserId id)
         {
             ...
         }
     }
  4. 인터페이스와 그 구현체가 되는 구상 클래스 사이에 나타나는 의존관계이다.
  5. 코드가 갖는 의존 관계UserApplicationServiceUserRepository객체를 속성으로 갖도록 정의됐다. 따라서 UserApplicationServiceUserRepository에 의존하는 상태다.UserRepository에서 특정 퍼시스턴시 기술에 의존을 하므로 UserApplication에 문제가 생긴다.추상 타입을 사용하면 기존에 구상 타입을 향하던 의존 관계 화살표가 추상 타입을 향하게 된다. 이런 식으로 의존 관계의 방향을 제어해 모든 모듈이 추상 타입에 의존하게끔 하면 비즈니스 로직이 특정 구현에서 해방될 수 있다.이렇게 의존 관계를 제어하는 방법을 의존 관계 역전 원칙 이라고 한다.
  6. public class UserApplicationService { private readonly IUserRepository userRepository; public UserApplicatoinService(IUserRepository userRepository) { this.userRepository = userRepository; } }
  7. 🙆‍♀️ 해결
  8. 🙅‍♀️ 문제
  9. public class UserApplicationService { private readonly UserRepository userRepository; public UserApplicatoinService(UserRepository userRepository) { this.userRepository = userRepository; } ... }

의존 관계 역전 원칙 : Dependency Inversion Principle

추상화 수준

입.출력으로부터의 거리이다.

거리가 가까우면 추상화 수준이 낮고, 거리가 멀수록 추상화 수준이 높다고 말한다.

추상 타입에 의존하라

UserApplicationService보다 UserRepository의 추상화 수준이 낮다. 하지만 위에서 본 예시과 같은 경우 추상화 수준이 높은 모듈이 추상화 수준이 낮은 모듈에 의존하고 있다.

이를 추상 타입을 도입하여 UserApplicationServiceUserRepository 두 클래스 모두 추상 타입을 향해 의존관계를 가지도록 한다. 이렇게 하면 “추상화 수준이 높은 모듈이 낮은 모듈을 의존해서는 안 되며 두 모듈 모두 추상 타입에 의존해야한다”를 만족하게 된다.

주도권을 추상 타입에 둬라

추상 타입이 세부 사항에 의존하면 낮은 추상화 수준의 모듈에서 일어난 변경이 높은 추상화 수준의 모듈까지 영향을 미치게 된다. 예를 들면 데이터스토어의 변경때문에 비즈니스 로직을 변경하는 상황이다.

주체가 되는 것은 추상화 수준이 높은 모듈, 추상타입이어야 한다. 추상화 수준이 낮은 모듈이 주체가 되어서는 안 된다.

추상화 수준이 높은 모듈은 추상화 수준이 낮은 모듈을 이용하는 클라이언트다. 클라이언트가 할 일은 어떤 처리를 호출하는 선언이다. 인터페이스는 구현할 처리를 클라이언트에 선언하는 것이며 주도권은 인터페이스를 사용할 클라이언트에 있다. 추상화 수준이 낮은 모듈을 인터페이스와 함께 구현하면 중요도가 높은 고차원적 개념에 주도권을 넘길 수 있다.

의존 관계 제어하기

public class UserApplicationService
{
    private readonly IUserRepository userRepository;

    public UserApplicationService()
    {
        this.userRepository = new InMemoryUserRepository();
        this.userRepository = new UserRepository();
    }
}

테스트용 인메모리 리포지토리를 사용하는 코드이다. userRepository가 추상 타입으로 정의되어 있지만, 생성자 메서드 안에서 구상 클래스의 객체를 만들면서 InMemoryUserRepository에 의존 관계가 발생한다. 이는 릴리즈시 코드 곳곳에서 테스트용 레포지토리를 운영용 레포지토리로 변경해야하는 수고로움이 있다.

Service Locator 패턴

Service Locator 패턴은 ServiceLocator객체에 의존 해소 대상이 되는 객체를 미리 등록해 둔 다음, 인스턴스가 필요한 곳에서 ServiceLocator를 통해 인스턴스를 받아 사용하는 패턴이다.

public class UserApplicationService
{
    private readonly IUserRepository userRepository;

    public UserApplicationService()
    {
        this.userRepository = ServiceLocator.Resolve<IUserRepository>();
    }
}

ServiceLocator 인스턴스에 요청을 통해 반환되는 인스턴스는 시작 스크립트 등을 이용해 미리 등록해둔다.

ServiceLocator.Register<IUserRepository, InMemoryUserRepository>();

Service Locator 패턴의 단점

  • 의존 관계를 외부에서 보기 어렵다.
  • 테스트 유지가 어렵다.

Dependency Injection 패턴

var userRepository = new InMemoryUserRepository();
var userApplicationService = new UserApplicationService(userRepository);

이 방법은 의존 관계를 주입하는 데 생성자 메서드를 사용하므로 생성자 주입이라고도 한다. 이 외에도 의존 관계를 주입할 수 있는 다양한 패턴이 존재한다.’

Dependency Injection 패턴의 장점

  • 의존 관계를 변경했을 때 코드 수정을 강제할 수 있다.

Dependency Injection 패턴의 단점

  • 의존하는 객체를 만드는 코드를 작성해야 한다. → IoC Container 패턴으로 해결한다.

IoC Container 패턴

var serviceCollection = new ServiceCollection();

serviceCollection.AddTransient<IUserRepository, InMemoryUserRepository>();
serviceCollection.AddTransient<UserApplicatoinService>();

var provider = serviceCollection.BuildServiceProvider();
var userApplicationService = provider.GetService<UserApplicationService>();

serviceCollection 은 IoC Container이다. IoC Container에 의존 관계 해소를 위한 설정을 등록해놓고, IoC Container로부터 필요한 인스턴스를 받아와서 사용한다. IoC Container의 설정은 Service Locator와 마찬가지로 시작 스트립트 등을 이용한다.

정리

‘소프트’웨어는 유연해야 한다. 의존 관계의 방향성을 제어하여 소프트웨어를 유연하게 만들자.

'디자인패턴' 카테고리의 다른 글

템플릿 메서드 패턴  (0) 2023.12.12
프록시 패턴  (0) 2023.08.28
싱글턴 패턴  (0) 2023.04.12