Injection 개발

본 장에서는 애플리케이션 개발과 유지보수를 용이하게 하는 DI(Dependency Injection) 패턴과 관련하여 ProObject에서 제공하는 기능들에 대해 설명한다.

1. Dependency Injection 개요

ProObject에서는 런타임 성능에 영향을 주지 않기 위하여 컴파일 시점에 추가적인 코드를 생성하는 방식을 사용한다.

사용자는 프레임워크로부터 제공받을 객체에는 @Inject로 표시해야 하고 @Inject로 표시된 member 변수들은 'new' 키워드로 객체를 생성할 필요가 없다.

2. Module

Module은 객체를 생성하는 역할을 담당한다.

  1. Module 클래스에는 반드시 @Module annotation을 선언해야 한다.

  2. 제공하는 객체를 리턴 타입으로 가지는 메소드를 선언하고 이 메소드에는 @Provides annotation을 선언해야 한다. 아래의 예제에서는 Water라는 객체를 리턴 타입으로 가지는 @Provides annoated 메소드를 생성한다.

  3. 객체가 LifeCycle을 가지는 경우에는 Scope annoation을 표시한다. Scope annotation이 없는 경우에는 매번 새로운 객체를 생성한다.

    아래의 예제에서는 애플리케이션 전체에서 공유되는 Water 객체를 제공하기 위해 @ApplicationScope annotation을 사용한다.

    package com.tmax.newdi.module;
    
    import com.tmax.app.Water;
    import com.tmax.proobject.di.module.Module;
    import com.tmax.proobject.di.module.Provides;
    import com.tmax.proobject.di.scope.ApplicationScope;
    
    @Module ----(1)
    public class ApplicationModule {
    
     @Provides  ----(2)
     @ApplicationScope ----(3) 
     public Water providesWater(){
      return new Water(40);
     }
    }

3. Component

component는 Lifecycle의 단위이며 실질적인 injection을 담당한다.

  1. component는 Lifecycle당 한개씩 인터페이스로 생성한다.

  2. component 인터페이스에도 Scope annotation을 표시해야 한다.

  3. component에는 계층 구조가 있을 수도 있고 상위 component에서 제공하는 객체를 하위 component에서도 사용하려면 상위 component에 리턴 타입을 포함한 메소드를 만들어야 한다.

    아래의 예제는 ApplicationScope의 component이며 하위 component에서도 application scope의 Water 객체를 사용할 수 있도록 Water를 리턴 타입으로 갖는 메소드를 선언해 놓은 것이다.

    package com.tmax.newdi.component;
    
    import com.tmax.app.Water;
    import com.tmax.newdi.module.ApplicationModule;
    import com.tmax.proobject.di.component.Component;
    import com.tmax.proobject.di.scope.ApplicationScope;
    
    @ApplicationScope
    @Component(modules = {ApplicationModule.class})
    public interface ApplicationComponent {
     Water providesWater();
    }

    아래의 예제는 component 간 계층 구조를 나타낸 것으로 dependencies에 지정된 ApplicationComponent가 HotelAComponent의 상위 component이다. 또한 ServiceGroupScope의 하위 scope에서 HotelA 객체를 사용할 수 있도록 HotelA를 리턴 타입으로 갖는 메소드를 선언한다.

    package com.tmax.newdi.component;
    
    import com.tmax.app.Water;
    import com.tmax.hotels.HotelA;
    import com.tmax.newdi.module.HotelAModule;
    import com.tmax.proobject.di.component.Component;
    import com.tmax.proobject.di.scope.ServiceGroupScope;
    
    @ServiceGroupScope
    @Component(modules = {HotelAModule.class}, dependencies = ApplicationComponent.class)
    public interface HotelAComponent {
     Water providesWater();
     HotelA providesHotelA();
    }
  4. component가 injection 대상으로 할 객체를 표시하기 위한 inject method를 생성해야 한다.

    아래 예제의 injectMakeCoffeeService가 inject method이며 메소드의 인자로 injection 대상 객체 타입을 지정해야 한다.

    package com.tmax.newdi.component;
    
    import com.devops.test.Customers;
    import com.tmax.newdi.module.CustomerModule;
    import com.tmax.proobject.di.component.Component;
    import com.tmax.proobject.di.component.RequestScopeComponent;
    import com.tmax.proobject.di.scope.ServiceRequestScope;
    import com.tmax.so.HotelAMakeCoffeeService;
    
    @ServiceRequestScope
    @Component(modules = {CustomerModule.class}, dependencies = {HotelAComponent.class})
    public interface HotelACoffeeServiceRequestComponent extends RequestScopeComponent{
     void injectMakeCoffeeService(HotelAMakeCoffeeService so);
     Customers providesCustomers();
    }

4. DI API 사용 방법

객체를 Injection하기 위해서는 component를 초기화하고 component에 선언된 inject method로 대상 객체를 호출한다. 만약 HotelAMakeCoffeeService를 대상으로 inject를 하려면 다음과 같은 형태가 된다.

ProObjectApplicationComponent appComponent
 = ProObjectApplicationComponent.initializer().applicationModule(
       new ApplicationModule()).initialize();

ProObjectHotelAComponent hotelAComponent
 = ProObjectHotelAComponent.initializer().applicationComponent(appComponent).
     hotelAModule(new HotelAModule()).initialize();

ProObjectHotelACoffeeServiceRequestComponent reuqustComponent
 = ProObjectHotelACoffeeServiceRequestComponent.initializer().
   hotelAComponent(hotelAComponent).customerModule(new CustomerModule()).initialize();

HotelAMakeCoffeeService so = new HotelAMakeCoffeeService();

reuqustComponent.injectMakeCoffeeService(so);
  1. 기본적인 형태는 'ProObject+{component interface 이름}.initializer().{모듈 추가}.initialize()'이다. 만약 component에 정의된 모듈이 여러 개인 경우에는 여러 개의 모듈을 모두 추가해야 한다.

  2. component가 계층 구조를 가진 경우에는 상위 component를 추가해야 한다.

    위의 예제에서 hotelAComponent의 .applicationComponent(appComponent)와 reqeustComponent의 hotelAComponent(hotelAComponent)가 그 과정이다.

  3. inject method를 가진 component로 대상 객체를 injection한다.

    위의 예제에서는 ProObjectHotelACoffeeServiceRequestComponent에 HotelAMakeCoffeeService을 대상으로 하는 inject method가 정의되어 있기 때문에 "reuqustComponent.injectMakeCoffeeService(so);"를 호출한다.

5. Custom Scope

ProObject에 정의된 scope(ApplicationScope, ServiceGroupScope, ServiceRequestScope)가 아닌 직접 정의한 scope를 사용하려면 scope annotation을 만들어야 한다.

  1. Scope annotation은 javax.inject.Scope annoation을 가진 @interface로 만들어야 한다.

    package com.tmax.userscope;
    
    import javax.inject.Scope;
    
    @Scope
    public @interface CoffeeOrderScope {
    
    }
  2. 모듈에 제공할 객체를 정의하고 필요하면 custom scope에서 정의한 annotation을 @Provides 메소드에 같이 적용한다.

    package com.tmax.newdi.module;
    
    import com.tmax.coffee.Menu;
    import com.tmax.coffee.Menu.MenuType;
    import com.tmax.proobject.di.module.Module;
    import com.tmax.proobject.di.module.Provides;
    import com.tmax.userscope.CoffeeOrderScope;
    
    @Module
    public class CoffeeOrderModule {
    
     private MenuType type;
    
     public CoffeeOrderModule(MenuType type) {
      this.type = type;
     }
    
     @Provides
     @CoffeeOrderScope
     Menu providesMenu(){
      return new Menu(type);
     }
    }
  3. Custom scope에서 정의한 annotation을 포함한 cmponent를 생성한다.

    package com.tmax.newdi.component;
    
    import com.tmax.coffee.Coffee;
    import com.tmax.newdi.module.CoffeeOrderModule;
    import com.tmax.proobject.di.component.Component;
    import com.tmax.userscope.CoffeeOrderScope;
    
    @CoffeeOrderScope
    @Component(modules = { CoffeeOrderModule.class })
    public interface CoffeeOrderComponent {
     void injectCoffee(Coffee coffee);
    }
  4. component를 초기화하고 대상 객체를 injection한다.

    ProObjectCoffeeOrderComponent coffeeComponent
      = ProObjectCoffeeOrderComponent.initializer().coffeeOrderModule(new CoffeeOrderModule(MenuType.Americano)).initialize();
    coffeeComponent.injectCoffee(coffee);

6. Lazy Injection

멤버 변수에 객체를 주입하는 방식은 실제 메소드 내에서 사용되지 않는 객체들도 생성되어 운영 환경에서 메모리, 성능에 부담되기 때문에 Lazy injection 기능을 제공한다.

Lazy<Type> 형태로 @Inject 변수를 선언해서 사용하면 @Inject 대상의 하위 의존 객체들은 주입되지 않는다. 사용자는 메소드 내에서 필요한 순간에 해당 Lazy 객체를 사용할 때 객체를 제공받게 된다. 이때 일반적인 메소드 호출방식과는 다르게 메소드를 호출하기전에 .get()을 한 번 호출해야 한다.

@BizObject(logicalName = "ClientTest")

public class ClientTest implements BusinessObject {
  @Inject
  Lazy<Client> client;

  public void test(){
    system.out.println("client name: " + client.get().getName());
  }
}

7. 예제

본 절에서는 간단한 애플리케이션을 작성하는 과정을 통해 Inject 사용법을 설명한다.

7.1. 개요

다음은 예제로 제공하는 애플리케이션의 구성이다.

figure di sample application
Sample Application LifeCycle
  • Application 객체

    Application scope와 Service group scope에 속한 객체이다.

  • Water 객체

    Application scope의 LifeCycle을 가지므로 이 애플리케이션에서 한 개만 존재하므로 Coffee Machine들에서 주입받아 사용하는 Water 객체들은 모두 동일하게 된다.

멤버 변수에 @Inject를 선언하면서 의존 관계가 생기게 된다. 다음은 이 애플리케이션의 리소스들간의 의존 관계를 나타낸 것이다.

image

7.2. 코드 작성

사용자가 만들어야 하는 코드는 Module, Component, Application Component Factory, ServiceGroup Component Factory, SO, BO이다.

7.2.1. Module 코드

다음은 Module 코드의 작성 예이다.

  • Application Module

    다음은 Application scope의 Water 객체를 제공하는 모듈의 코드이다.

    image

  • HotelA Module

    다음은 HotelA의 Service group scope에 속하는 객체들을 제공하는 모듈의 코드이다.

    image

  • HotelB Module

    다음은 HotelB의 Service group scope에 속하는 객체들을 제공하는 모듈의 코드이다.

    image

  • Customer Module

    다음은 Service request scope에 속하는 Customers 객체를 제공하는 모듈의 코드이다.

    image

  • CoffeeOrder Module

    다음은 User scope에 속하는 Menu 객체를 제공하는 모듈의 코드이다.

    image

7.2.2. Component 코드

다음은 Component 코드의 작성 예이다.

  • Application Component

    다음은 Application scope의 component의 코드이다. 하위 component에서도 Water 객체를 제공할 수 있도록 메소드가 정의되어 있다.

    image

  • HotelA Component

    다음은 Service group scope 중 HotelA의 component의 코드이다. 하위 객체에서도 Water 및 HotelA 객체를 제공할 수 있도록 메소드들이 정의되어 있다.

    image

    상위 component인 Application Component에서 제공하는 Water 객체를 ServiceGroup scope인 현재 scope 하위 scope에 전달하려면 여기에서도 Water를 리턴으로 가지는 메소드를 정의해야 한다.

  • HotelB Component

    Service group scope 중 HotelB의 component이다.

    image

  • HotelACoffeeServiceRequest Component

    HotelAComponent ServiceGroup을 상위 scope로 가지는 ServiceRequestScope의 component이다. SO를 대상으로 하는 inject method가 정의되어 있다.

    image

  • HotelBCoffeeServiceRequest Component

    HotelBComponent ServiceGroup을 상위 scope로 가지는 ServiceRequestScope의 component이다. SO를 대상으로 하는 inject method가 정의되어 있다.

    image

    RequestScope의 component를 정의할 때에는 반드시 RequestScopeComponent를 상속받는 인터페이스로 생성해야 한다.

  • CoffeeOrder Component

    user scope(CoffeeOrderScope)의 component이며 계층관계를 가지지 않는 독립 Component이다.

    image

7.2.3. Factory 코드

다음은 ApplicationComponentFactory와 ServiceGroupComponentFactory 코드의 예이다.

  • ApplicationComponentFactory

    Application scope의 component들과 Service group scope의 component들은 런타임에서 singleton 객체로 존재하여야 하기 때문에 이 component들을 관리하는 factory를 생성해야 한다.

    image

  • ServiceGroupComponentFactory

    service group의 component는 singleton 객체로 관리되어야 하므로 singleton pattern으로 service group의component들을 관리하는 팩토리 클래스를 작성한다. 초기화 과정에서 상위 component인 Application component를 채워줘야 한다.

    image

7.2.4. SO, BO 코드

다음은 실제로 @Inject를 사용하여 객체를 제공받는 SO, BO 영역들 코드의 예이다.

  • Water

    application scope로 제공되는 water 객체이다. scope annotation은 SO, BO 클래스들이 아닌 module 에 정의된다.

    image

  • CoffeeMachine

    CoffeeMachine은 service group scope에 속하게 되므로 HotelA와 HotelB에 서로 다른 객체가 제공 되지만 Water 객체는 application scope이므로 HotelA의 coffeeMachine과 HotelB의 coffeeMachine의 water 객체는 같은 객체를 받게 된다.

    image

  • CafeA1

    CafeA1과 CoffeMachine모두 service group scope에 속하게 되므로 CafeA1은 CoffeMachine을 @Inject member 변수로 가질 수 있다.

    image

    하위 scope에 속한 객체는 @Inject member 변수로 가질 수 없다. 이 예에서 만약 CafeA1에 service request scope인 Customers를 @Inject member 변수로 선언하게 되면 컴파일 에러가 발생하게 된다.

  • Restaurant

    Cafe들의 상위 클래스이다.

    image

  • HotelA

    Hotel 역시 service group scope이기 때문에 Cafe를 @Inject 대상으로 가질 수 있다.

    image

    RequestScope의 component를 정의할 때에는 반드시 RequestScopeComponent를 상속받는 인터페이스로 생성해야 한다는 것에 주의한다.

  • Hotel

    Hotel들의 상위 클래스이다.

    image

  • HotelAMakeCoffeeService

    SO의 inject method는 service request scope의 component에 정의되어 있기 때문에 SO의 Constructor에서 request component를 초기화한 후에 SO를 injection해 주는 코드를 작성한다.

    image

    request component의 상위 component인 ProObjectHotelAComponent를 반드시 초기화 과정에서 넣어 주어야 한다. 또한 request scope의 component들은 초기화 후에 반드시 ServiceContext에 넣어줘야 한다.

  • HotelAMakeCoffeeService

    service method에서는 인자로 넘어온 customers DataObject를 request scope component에 있는 request scope에서 유지되는 Customers 객체로 복사 후 사용한다. request scope component를 얻어오는 API는 ServiceContext에 있으며 실질적인 reqeust component 구현체로 캐스팅하여 사용해야 한다. request scope component에 속한 객체를 사용하기 위해서는 component interface에 정의해 놓은 method를 사용하면 된다.

    image

7.3. 검증 기능

ProStudio(개발툴)에서 코드 작성 후에 저장하면 자동으로 컴파일된다. 컴파일 때 Java 문법에는 맞더라도 Depdency Injection 기능을 사용하는 경우 문법에 어긋나는 경우를 컴파일 에러로 검증하는 과정이 필요하다.

본 절에서는 컴파일할 때 발생할 수 있는 에러에 대해서 설명한다.

7.3.1. binding check

component 내의 @Inject 대상 객체를 제공하는 @Provides method가 없는 경우에는 다음과 같이 compile error가 발생한다.

HotelA에 @inject 대상 CafeA1이 있는데 component에 등록된 모듈들 중에서 CafeA1을 제공하는 메소드가 없는 경우이다. 메소드는 놔둔 채 @Provides annotation만 삭제해도 동일한 결과가 된다.

figure di validaiton no binding1
HotelA Module에서 CafeA1을 제공하는 메소드 삭제
figure di validaiton no binding2
HotelAComponent 빌드할 때 컴파일 에러 발생

하위 scope의 객체를 @Inject 대상으로 갖는 경우에도 현재 scope에서 대상 객체를 찾을 수 없다고 판단한다. 아래의 경우는 Service group scope의 리소스인 Cafe에서 하위 scope인 Service request scope의 Customers를 @Inject 대상으로 가지고 있는 경우이다.

figure di validaiton no binding3
CafeA2에 하위 component에서 제공하는 객체 추가
figure di validaiton no binding4
HotelAComponent 빌드할 때 컴파일 에러 발생

7.3.2. multi binding check

상위 component인 application component에 등록된 모듈에서 제공하는 Water 객체를 service group component에 등록된 모듈에서 또 제공하는 경우에는 컴파일 에러가 발생한다.

component에 등록된 모듈 내에서 중복으로 동일한 객체를 제공하는 경우도 마찬가지이다.

figure di validaiton multi binding1
한 component에서 중복하여 동일한 객체를 제공
figure di validaiton multi binding2
component에서 중복하여 동일한 객체를 제공한 경우 컴파일 에러 발생

7.3.3. cycle check

component 내의 모듈에서 제공하는 객체들 중에 cyclic depdendency가 존재하는 리소스가 배포되어 실행 되면 런타임에서 무한루프에 빠지게 되기 때문에 이를 방지하기 위해 컴파일 시점에 에러가 발생하게 된다.

다음의 예에서는 CoffeMachine에 @Inject member 변수로 HotelA를 추가함으로써 "HotelA → CafeA2 → CoffeMachine → HotelA"의 cycle이 생기게 된다.

figure di validaiton cyclic dependency1
cyclic depdendency 발생
figure di validaiton cyclic dependency2
cyclic dependency를 포함한 경우 component에서 compile 에러 발생

7.4. 리소스 배포 및 관리

컴파일된 코드와 자동 생성되는 Component 구현체, Injector, Provider 코드 등도 배포되어야 정상적으로 작동한다. 본 절에서는 Dependency Injection 관련 리소스 배포 및 관리하는 방법을 설명한다.

7.4.1. 프로젝트 설정

ProStudio에서 프로젝트를 선택한 후 컨텍스트 메뉴에서 [Properties]를 선택한다.

  1. 메뉴 트리에서 [Java Compiler] > [Annotation Processing]에서 다음과 같이 설정되어 있는지 확인한다.

    figure di project configurations
    Project Annotation Processing 설정 확인
  2. 메뉴 트리에서 [Java Compiler] > [Annotation Processing] > [Factory Path]에서 다음과 같이 설정되어 있는지 확인한다.

    figure di project configurations2
    Project Factory Path 설정 확인

7.4.2. 리소스 배포

사용자가 Component, Module, SO, BO 등을 개발하면 .po7_generated 폴더에 Dependency Injection 관련 리소스가 생성된다.

생성되는 리소스는 Component 구현체, *Injector, *Provider이다. 이 리소스들을 일반 pojo java 파일처럼 git index에 추가한 후에 commit, push하여 배포한다.

figure di generated resources
자동 생성된 DI 관련 소스 코드

7.4.3. 리소스 재배포

자동 생성되는 component 구현체, Injector, Provider는 서로 영향을 받아 변경될 수 있으므로 변경된 경우에는 다시 배포해야 한다. component는 다른 리소스들의 변경에 의해 영향을 받아 자주 변경되므로 반드시 다른 리소스들 배포 이후 component가 변경되었는지 확인해서 변경된 경우 재배포가 필요하다. component에서 컴파일 에러가 발생하는 경우에는 component interface를 다시 저장해서 빌드한 후에 자동 생성된 구현체를 다시 배포해야 한다. 자동 생성되는 코드들은 사용자가 작성하지 않으므로 사용자가 모르는 사이에 변경될 수 있고 자동 생성된 코드들이 운영계로 배포되지 않으면 정상작동하지 않는다.

다음의 경우 자동생성된 코드가 재생성된다.

  1. 사용자가 작성하는 component interface를 변경하면 component 구현체가 다시 생성된다.

  2. 모듈을 변경하면 연관 provider들이 다시 생성된다.

  3. 모듈을 변경하면 변경된 모듈이 등록된 component들의 구현체가 다시 생성된다.

  4. @Inject를 사용하는 SO, BO 또는 pojo java가 변경되면 연관 injector들이 다시 생성된다.

  5. @Inject를 사용하는 SO, BO 또는 pojo java가 변경되면 변경된 클래스 타입의 객체를 제공하는 모듈이 등록된 component가 다시 생성된다.