2016/02/29 - [java] - 단순한 Injector를 구현해 써보자 #1


이전 글은 윗 링크에 있습니다. 앞으로 작성할 코드는 아래 GitHub repository에 저장되어 있으니 참고 하세요.


우선 제가 만들 Dependency Injector는 팩토리 패턴과 싱글톤 패턴을 이용한 아주 Naive한 Injector 입니다. 사실 진정한 dependency injector 아니고 무늬만 JSR 330 스펙을 조금 따르는 코드입니다. 이를 이용해 샘플 코드를 구현해 보고 문제점을 보도록 하겠습니다. 그런 후 JSR 330에 소개된  annotation을 처리하기 위해 apt(annotation processing tool)을 활용하여 실제 dependency injection이 수행되도록 구현하겠습니다. (이건 개인적으로 단순 호기심과 취미로 구현하려는 것이니 실제 자바 응용 개발하실 땐 Dagger2 또는 Spring을 사용하셔야 합니다.) 본 글은 JSR 330에 대한 지식이 있는 것으로 가정하고 모르시면 http://docs.oracle.com/javaee/6/api/javax/inject/package-summary.html 을 읽으시면 됩니다.


Car, Weather, Road, GangnamRoad, SamsungRoad 로 구성된 애플리케이션을 개발하는데 각 클래스는 다음과 같이 관계를 가지고 있습니다. 


DI는 syringe가 해야합니다. 사실 syringe가 해야하는데 syringe가 알아서 하도록 하려면 reflection을 사용하여 런타임에 DI가 수행되도록 하거나 apt(annotation processing tool)로 DI를 수행하는 코드를 생성할 수 있는데 먼저 우리가 가장 쉽게할 수 있는 DI 흉내만 내어보겠습니다. 

Car, Road, GangnamRoad, SamsungRoad, Weather 클래스는 아래와 같습니다.

public class Car {
    @Inject Weather weather;
    Road road;

    @Inject
    public Car() {
        weather = Syringe.getInstance().provideWeahter();
    }

    public void setRoad(Road road) {
        this.road = road;
    }

    public void drive() {
        if(road.canGo()) {
            System.out.println("I'm driving on " + road);
        } else {
            System.out.println("I can't drive on " + road);
        }

        weather.printWeather();
    }

    public void stop() {
        System.out.println("I stop on " + road);
        weather.printWeather();
    }
}

@Singleton
public class Weather {
    String state;

    public void setState(String state) {
        this.state = state;
    }

    public void printWeather() {
        System.out.println("current weather is " + state);
    }
}

Weather 클래스는 싱글톤인데요. JSR 330에서 싱글톤 클래스는 @Singleton annotation만 추가하면 DI 프레임워크에서 해당 클래스를 싱글톤으로 만들어줘야 한다고 되어 있으니, getInstance() 메소드를 만드는 작업은 이제부터 하지말도록 합니다. 어떤 클래스를 싱글톤으로 만들고 싶다? 그럴 땐 @singleton만 붙여주시면 만사오케이! 입니다. 

public interface Road {
    boolean canGo();
}

public class GangnamRoad implements Road {
    public boolean canGo() {
        return true;
    }

    @Override
    public String toString() {
        return GangnamRoad.class.getSimpleName();
    }
}

public class SamsungRoad implements Road {
    public boolean canGo() {
        return false;
    }

    @Override
    public String toString() {
        return SamsungRoad.class.getSimpleName();
    }
}

GangnamgRoad, SamsungRoad, Weather 클래스들은 dependency를 필요로 하지 않기 때문에 DI가 필요없고 Car 클래스는 Weather, Road 객체를 사용해야 하기 때문에 DI가 필요합니다. 그래서 "@Inject Weather weather;"로 syringe가 Weather 인스턴스를 주입하도록 구현했고 Car constructor에서 syringe를 통해 Weather 인스턴스를 주입하게 됩니다. Car 객체가 syringe에 의해 생성될 때는 인자가 없는 생성자가 호출되도록 "@Inject public Car() { ... }" 로 구현을 했습니다. DI 개념의 도입으로 프로그램의 핵심을 구성하는 클래스 내부에서 new란 키워드로 객체를 생성하는 부분이 없어졌습니다. 앱을 실행 하는 코드를 먼저 살펴보고 보겠습니다.

public class App {
    @Inject Car car;
    @Inject @Named("gangnam") Road gangnamRoad;
    @Inject @Named("samsung") Road samsungRoad;
    @Inject Weather weather;

    public App() {
        car = Syringe.getInstance().provideCar();
        gangnamRoad = Syringe.getInstance().provideGangnamRoad();
        samsungRoad = Syringe.getInstance().provideSamsungRoad();
        weather = Syringe.getInstance().provideWeahter();
    }

    public void run() {
        car.setRoad(gangnamRoad);
        weather.setState("raining");
        car.drive();
        weather.setState("cloudy");
        car.stop();

        car.setRoad(samsungRoad);
        car.drive();
    }

    public static void main(String[] args ) {
        App app = new App();
        app.run();
    }
}

위 프로그램을 실행 시키면 짜잔. 아래와 같은 결과과 나옵니다.

I'm driving on GangnamRoad
current weather is raining
I stop on GangnamRoad
current weather is cloudy
I can't drive on SamsungRoad
current weather is cloudy

위 프로그램을 보시면 음?? 객체는 어디에서 생성되서 실행되는 거야? 라는 의문이 생길것입니다. 객체 생성은 모두 DI 프레임워크가 담당하고 저는 syringe란 놈을 만들면서 syringe에서 객체 생성이 모두 이루어 지도록 할 예정입니다. App클래스의 생성자를 보면 App클래스에서 사용하는 모든 클래스의 객체 주입이 syringe에 의해 수행되고 있습니다. 현재의 Syringe 클래스는 단지 팩토리 메소드로 객체를 생성하는 것이라 개발자가 어떤 dependency를 필요로 하면 dependency를 주입까지 해줘야 하는 번거로움이 있습니다. 물론 Syringe 클래스에 해당 팩토리 메소드를 만들어야 하는 일까지 해야합니다. 아래 Syringe 클래스를 보시죠.

public class Syringe {
    static Syringe instance;
    Map<Class<?>, Object> singletonCache;

    private Syringe() {
        singletonCache = new HashMap<Class<?>, Object>(100, 0.75f);
    }

    public static Syringe getInstance() {
        if(instance == null) {
            synchronized (Syringe.class) {
                if(instance == null) {
                    instance = new Syringe();
                }
            }
        }

        return instance;
    }

    public Weather provideWeahter() {
        Object object = singletonCache.get(Weather.class);
        if(object == null) {
            synchronized (Weather.class) {
                if((object = singletonCache.get(Weather.class)) == null) {
                    object = new Weather();
                    singletonCache.put(Weather.class, object);
                }
            }
        }

        return (Weather)object;
    }

    public Car provideCar() {
        return new Car();
    }

    public GangnamRoad provideGangnamRoad() {
        return new GangnamRoad();
    }

    public SamsungRoad provideSamsungRoad() {
        return new SamsungRoad();
    }
}

각 dependency들을 주입하기 위해 객체를 리턴하는 팩토리 메소드를 한땀한땀 만들었습니다. 싱글톤을 구현하기 위해서 com.util.HashMap을 사용해 싱글톤 캐쉬를 만들었습니다. 그래서 @singleton annotation으로 annotated된 클래스들은 getInstance() 메소드를 구현하지 않고 syringe가 알아서 처리해 주도록 처리하였습니다.


이렇게 팩토리 메소드로 DI를 구현하려니 너무 문제가 많습니다. 개발자는 @Inject annotation만 사용해서 dependency를 주입하고 싶은데 syringe에 직접 객체 생성코드를 작성해야 하고 Syringe.provide~() 메소드를 명시적으로 호출해 DI를 수행해야 합니다. 예를 들면 Car 클래스 생성자에서 Syringe.provide~(), App 클래스 생성자에서 Syringe.provide~() 메소드 호출이 필요없이 Syringe가 자동으로 객체와 참조를 바인딩해줘야 합니다. 그럼 개발자는 아래와 같이 Car, App 클래스를 구현해주면 되겠죠. 그리고 이게 제가 원하는 방식이고 JSR 330에 명시된 요구사항입니다. 그런데 이렇게 하려면 이제 apt가 필요합니다. Syringe 클래스에 팩토리 메소드를 하나씩 추가하는게 아닌 apt를 이용해 객체를 주입하고 생성하는 코드를 생성할 필요가 생겼습니다. 저는 DI 프레임워크 구현할 때 리플렉션을 사용하지 않을 겁니다. Roboguice가 리플렉션으로 DI 구현하다 성능 이슈가 생겨서 개발자들에게 외면 받았다는걸 상기시키면 리플렉션 접근은 이 분야에선 피해야 겠습니다.(RoboGuice 개발자도 리플렉션을 피하고 apt를 사용하려고 작업 중인걸로 압니다..) 다음 포스트에서는 apt가지고 DI 만들기에 접근해 보겠습니다.

public class Car {
    @Inject Weather weather;
    Road road;

    @Inject
    public Car() {
    }

    public void setRoad(Road road) {
        this.road = road;
    }

    public void drive() {
        if(road.canGo()) {
            System.out.println("I'm driving on " + road);
        } else {
            System.out.println("I can't drive on " + road);
        }

        weather.printWeather();
    }

    public void stop() {
        System.out.println("I stop on " + road);
        weather.printWeather();
    }
}

public class App {
    @Inject Car car;
    @Inject @Named("gangnam") Road gangnamRoad;
    @Inject @Named("samsung") Road samsungRoad;
    @Inject Weather weather;

    public App() {
    }

    public void run() {
        car.setRoad(gangnamRoad);
        weather.setState("raining");
        car.drive();
        weather.setState("cloudy");
        car.stop();

        car.setRoad(samsungRoad);
        car.drive();
    }

    public static void main(String[] args ) {
        App app = new App();
        app.run();
    }
}


Posted by 제이제이랩
,