반응형

저번 포스팅에 이어 DI 패턴과 IoC Container에 대해 알아보겠다.

Step 3. DI (Dependency Injection)

DI(Dependency Injection, 의존관계 주입)는 종속된 객체의 생성을 반전시키는 IoC 원칙을 구현한 디자인 패턴이다. 이전에 DIP 원칙에 따라 추상화하여 클래스간의 결합도를 낮추었다. 하지만 이전 포스팅 예제에서 여전히 CustomerBusinessLogic 클래스가 ICustomerDataAccess 객체를 반환하기 위해 DataAccessFactory 클래스를 참조하고 있다. 이제 DI 패턴을 구현하여 종속된 클래스의 객체 생성을 완전히 클래스 밖으로 내보낼 수 있다.

DI 패턴은 IoC를 구현하는 디자인 패턴 중 하나이다. DI를 통해 종속된 객체 생성을 클래스 외부로 분리하고, 해당 클래스에서 객체를 사용할 수 있는 다른 방법들을 제공한다.

DI 패턴은 세 가지 유형의 클래스에 관여함 (The Dependency Injection pattern involves 3 types of classes)

  • Client 클래스 (dependent) : Service 클래스에 의존하는 클래스
  • Service 클래스 (dependency) : Client 클래스에게 서비스를 제공하는 클래스
  • Injector 클래스 : Service 클래스의 객체를 Client 클래스에 주입하는 클래스

Injector 클래스로서 CustomerService 클래스를 만들고, Service 클래스인 CustomerDataAccess 클래스의 객체를 Client 클래스인 CustomerBusinessLogic 에게 세 가지 방법으로 주입해보겠다.
DI를 통해 코드는 더 깔끔하고, 느슨한 결합(decoupling)은 의존성을 가진 객체를 제공할 때 더 큰 효과를 발휘한다. 객체는 더 이상 의존성을 살펴보거나, 그리고 의존성을 가진 클래스나 위치를 알 필요가 없다. 결과적으로 클래스는 테스트하기 더욱 용이해진다.

DI 패턴 적용 3 가지 방법

Injector 클래스는 크게 세 가지 방법으로 객체를 주입할 수 있다.

  • 생성자 주입(Constructor Injection) : Injector 클래스가 Client 클래스의 생성자를 통해 Service 클래스(dependency)의 객체를 주입함
  • Property Injection : Setter Injection처럼 Client 클래스의 public property를 통해 객체를 주입함
  • 메서드 주입(Method Injection) : Client 클래스가 어떤 인터페이스를 구현하여 어떤 메소드가 종속성을 제공하는 지 선언하고, Injector 클래스는 이 인터페이스를 사용하여 Client 클래스에 종속성을 제공함

DI 예제를 통해 이전 포스팅에서 남아있던 클래스 간의 결합도를 완전히 낮추어보겠다.
Injector 클래스로서 CustomerService 클래스를 만들고, Service 클래스인 CustomerDataAccess 클래스의 객체를 Client 클래스인 CustomerBusinessLogic 에게 세 가지 방법으로 주입해보자.

 

1. 생성자 주입(Constructor Injection)

CustomerService 클래스가 CustomerDataAccess 클래스의 객체를 생성하여 CustomerBusinessLogic 클래스에게 의존 관계를 주입한다. 이제 CustomerBusinessLogic 클래스는 new 키워드로 CustomerDataAccess 클래스 객체를 생성하거나 Factory 클래스인 DataAccessFactory 를 사용할 필요가 없어졌다. 이로서 CustomerDataAccessCustomerBusinessLogic 클래스는 더욱 더 낮은 결합도를 가지가 되었다.

CustomerBusinessLogic

public class CustomerBusinessLogic {
    private ICustomerDataAccess customerDataAccess;

    public CustomerBusinessLogic(ICustomerDataAccess customerDataAccess) {
        this.customerDataAccess = customerDataAccess;
    }

    public CustomerBusinessLogic() {
        customerDataAccess = new CustomerDataAccess();
    }

    public String processCustomerName(int id) {
        return customerDataAccess.getCustomerName(id);
    }
}


CustomerService
(신규로 추가되었다)

public class CustomerService {
    private CustomerBusinessLogic customerBusinessLogic;

    public CustomerService() {
        customerBusinessLogic = new CustomerBusinessLogic(new CustomerDataAccess());
    }

    public String getCustomerName(int id) {
        return customerBusinessLogic.processCustomerName(id);
    }
}


2. Property Injection

의존관계를 public한 필드를 통해 제공하는 것을 Property Injection이라고 한다. CustomerBusinessLogicICustomerDataAccess 타입의 public 필드 dataAccess에 대한 getter, setter 메서드를 구현한다. 그래서 CustomerService 클래스가 public한 필드인 dataAccess를 통해 CustomerDataAccess 클래스 객체 생성을 할 수 있게 된다.


CustomerBusinessLogic

public class CustomerBusinessLogic {

    public ICustomerDataAccess dataAccess;

    public CustomerBusinessLogic() {
    }

    public void setDataAccess(ICustomerDataAccess dataAccess) {
        this.dataAccess = dataAccess;
    }

    public String getCustomerName(int id) {
        return dataAccess.getCustomerName(id);
    }
}


CustomerService
(신규로 추가되었다)

public class CustomerService {
    private CustomerBusinessLogic customerBusinessLogic;

    public CustomerService() {
        customerBusinessLogic = new CustomerBusinessLogic();
        customerBusinessLogic.dataAccess = new CustomerDataAccess();
    }

    public String getCustomerName(int id) {
        return customerBusinessLogic.getCustomerName(id);
    }
}


3. Method Injection

클래스 메서드나 인터페이스 메서드를 통해 의존관계를 주입할 수 있다. 아래 예제는 인터페이스 메서드를 통한 예제이다. 아래 예제에서는 CustomerBusinessLogic 클래스가 setDependency() 메서드를 갖고 있는 IDataAccessDependency 인터페이스를 구현한다. 그래서 Injector 클래스인 CustomerService 가 setDependency() 메서드를 통해 의존 관계를 주입할 수 있다.


CustomerBusinessLogic

public class CustomerBusinessLogic implements IDataAccessDependency {
    private ICustomerDataAccess customerDataAccess;

    public CustomerBusinessLogic() {
    }

    public String getCustomerName(int id) {
        return customerDataAccess.getCustomerName(id);
    }

    @Override
    public void setDependency(ICustomerDataAccess customerDataAccess) {
        this.customerDataAccess = customerDataAccess;
    }
}

IDataAccessDependency (신규로 추가함)

public interface IDataAccessDependency {
    void setDependency(ICustomerDataAccess customerDataAccess);
}


CustomerService
(신규로 추가함)

public class CustomerService {
    private CustomerBusinessLogic customerBusinessLogic;

    public CustomerService() {
        customerBusinessLogic = new CustomerBusinessLogic();
        customerBusinessLogic.setDependency(new CustomerDataAccess());
    }

    public String getCustomerName(int id) {
        return customerBusinessLogic.getCustomerName(id);
    }
}


TA-DA!
이제 DI와 Strategy 패턴을 통해 클래스간의 결합도를 낮출 수 있게 되었다.

지금까지 클래스 간의 결합도를 낮추기 위해 여러 개의 원칙(IoC, DIP)과 패턴(DI, Strategy)을 사용했다. 실제 업무에서는 더 많은 의존 관계가 존재하기 때문에 이런 패턴들을 적용하는 데에 시간이 많이 소요된다. 그래서 IoC Container (aka the DI container) 가 우리를 도와준다 😉! 이제 IoC Container 개념에 대해 배워보자!


Step 4. IoC Container

IoC Container(a.k.a DI Container)는 DI를 자동으로 관리해주는 프레임워크이다. IoC Container는 객체의 생성과 생명주기, 그리고 클래스에 해당 객체의 의존 관계를 주입하는 것 까지 관리한다.

지금까지 클래스 간의 결합도를 낮추기 위해 여러 개의 원칙(IoC, DIP)과 패턴(DI)을 사용했다. 하지만 실제 업무에서는 더 많은 의존 관계가 존재하기 때문에 이런 패턴들을 적용하는 데에 시간이 많이 소요된다.

IoC Container는 우리들 대신 특정 클래스들의 객체를 생성하고, Constructor, Property, Method를 통해 runtime 시점에 혹은 적당한 시점에 해당 클래스에 의존성을 가진 클래스들에게 의존 관계를 주입해준다. 그렇기 때문이 우리가 직접 객체를 생성하거나 관리하지 않아도 된다.

모든 컨테이너들은 반드시 다음과 같은 DI 생명주기에 대해 쉽게 사용할 수 있도록 지원해야한다.

  • Register : The container must know which dependency to instantiate when it encounters a particular type. This process is called registration. Basically, it must include some way to register type-mapping.
  • Resolve : When using the IoC container, we don't need to create objects manually. The container does it for us. This is called resolution. The container must include some methods to resolve the specified type; the container creates an object of the specified type, injects the required dependencies if any and returns the object.
  • Dispose : The container must manage the lifetime of the dependent objects. Most IoC containers include different lifetimemanagers to manage an object's lifecycle and dispose it.

 

Spring IoC Container

시중에는 많은 Ioc Container가 존재하지만, Spring 프레임워크에서 제공하고 있는 공식 문서를 기준으로 IoC Container에 대해 알아보자.IoC Container는 이전 단계들에서 적용한 IoC 원칙, DIP 원칙, DI 패턴을 우리 대신 적용해준다. 하지만 어떻게 IoC Container가 우리가 원하는 것을 마법처럼 동작해줄 수 있을까?우리가 IoC Container에게 제공해줘야 하는 것은 의존 관계에 대한 메타 데이터(the configuration metadata)이다. Spring IoC Container는 우리가 제공한 메타데이터를 읽어들여 인스턴스화(instantiating), 구성(configuring), 그리고 빈들을 정리(assembling the beans) 한다.

Spring IoC Container 동작

다음 다이어그램은 Spring IoC Container가 어떻게 동작하는 지 추상화하여 보여준다. org.springframework.context.ApplicationContext 인터페이스는 Spring IoC Container를 나타낸다. 클래스들은 구성에 대한 메타 데이터와 함께 결합되어, ApplicationContext가 생성되고 초기화된 이후에는 드디어 실행 가능하고 온전히 구성되어 있는 어플리케이션이 된다.

https://camo.githubusercontent.com/d0e90ed61f083b0ae84d723a89310ffe370149a8a8c50ebb9e4f97fdd579a3f4/68747470733a2f2f646f63732e737072696e672e696f2f737072696e672d6672616d65776f726b2f646f63732f63757272656e742f7265666572656e63652f68746d6c2f696d616765732f636f6e7461696e65722d6d616769632e706e67

 

Spring 메타데이터 설정방법

Spring DI

Spring IoC Container는 ApplicationContext 인터페이스를 DI를 구현하는데, 크게 constructor-basedsetter-based 방식이 있다.


생성자 기반 의존관계 주입(Constructor-based Dependency Injection)

public class SimpleMovieLister {

  // the SimpleMovieLister has a dependency on a MovieFinder
  private final MovieFinder movieFinder;

  // a constructor so that the Spring container can inject a MovieFinder
  public SimpleMovieLister(MovieFinder movieFinder) {
      this.movieFinder = movieFinder;
  }

  // business logic that actually uses the injected MovieFinder is omitted...
}


세터 메서드 기반 의존관계 주입(Setter-based Dependency Injection)

public class SimpleMovieLister {

  // the SimpleMovieLister has a dependency on the MovieFinder
  private MovieFinder movieFinder;

  // a setter method so that the Spring container can inject a MovieFinder
  public void setMovieFinder(MovieFinder movieFinder) {
      this.movieFinder = movieFinder;
  }

  // business logic that actually uses the injected MovieFinder is omitted...
}


Constructor-based or setter-based DI?

Spring 공식문서 에 따르면 생성자 주입 방식은 필수적인 의존성을 주입할 때 사용하고, 세터 메서드 방식은 옵셔널한 의존성을 주입할 때 사용하기를 추천한다.

Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies. Note that use of the @Required annotation on a setter method can be used to make the property be a required dependency; however, constructor injection with programmatic validation of arguments is preferable. The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns. Setter injection should primarily only be used for optional dependencies that can be assigned reasonable default values within the class. Otherwise, not-null checks must be performed everywhere the code uses the dependency. One benefit of setter injection is that setter methods make objects of that class amenable to reconfiguration or re-injection later. Management through JMX MBeans is therefore a compelling use case for setter injection. Use the DI style that makes the most sense for a particular class. Sometimes, when dealing with third-party classes for which you do not have the source, the choice is made for you. For example, if a third-party class does not expose any setter methods, then constructor injection may be the only available form of DI.

참고 자료

반응형