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 제이제이랩
,

자바로 웹 프론트 엔드, 백 엔드를 다루시는 분들이라면 필히 스프링의 Dependency Injection은 많이 다뤄보셨으리라 봅니다. 저 같은 경우는 안드로이드 native 개발자로 첫 단추를 풀어서 인지 몇년이 지나도록 DI에 대한 개념과 필요성을 인식하지 못했습니다. 물론 저의 게이름과 안일함 때문이긴 한데요;;;


이전에 벤처에 있을 때 여러 오픈소스를 탐닉하다 RoboGuice를 앱에 적용해 보고 DI 프레임 워크의 편리함을 맛 보았습니다. ButterKnife 또한 사용해 보면서 그 편리함에 감탄했었는데요. RoboGuice가 Guice로부터 파생된 걸 알게 되었고 런타임 도중 reflection의 과도한 사용으로 고질적으로 앱의 성능에 치명적인 단점을 제공하고 있었습니다. 다른 프레임워크를 찾다 Square에서 만든 Dagger를 접하였습니다. 구글에서 Guice를 대체할 DI 프레임워크로 Dagger에 주목하여 지금은 구글 core library team에서 Dagger 2를 주도하여 개발하고 있습니다. 


http://google.github.io/dagger/ [Dagger 2 설명보기]


런타임 Reflection을 완전히 없애고 Annotation processing으로 code generation해서 성능상 문제가 없게 만들었습니다. 모바일, 서버, 일반 자바 애플리메이션 개발에서 쓸 수 있습니다. 저야 저의 샘플앱에 Dagger, Dagger 2를 둘다 적용해 봤지만 지금도 이해가 되지 않는게 많답니다. 아직도 Object graph가 머리속에 그려지지 않네요. 


아무튼 회사에서 제가 드디어 테스트 코드를 짜면서 개발을 하게 된지라.(개발 6년차인데 지금 짜고 있습니다;;) 이전에는 샘플 프로젝트에 먼저 만들 코드 작업하고 실행시켜보고 이상이 있나 없나 검증한뒤에 실제 앱 코드에 적용하는 방식을 썼었습니다. QA팀에서도 버그가 걸러져 나오고 이러한 방식에 익숙하다 보니 앱 개발하면서 unit test, instrumentation test 코드 작성에 대한 필요성을 많이 못 느끼고 있었죠. 그 시간에 요구사항 개발하기도 바쁘고 현재 앱의 수많은 버그 잡는데 더 시간을 쏟겠다라는게 기본 철학이었죠. 현 회사에서 앱 시작 속도 개선이라는 메이저급 변경을 하면서 사이드 이펙트가 상당히 크게 나고 긴급 배포 2번이나 하면서 이대론 안되겠다 테스트 코드밖에 답이 없겠구나 절실히 느꼈습니다;;


그래서 이번에 저에게 떨어진 일은 junit4, hamcrest, mockito, robolectric, espresso 기반으로 테스트 코드를 작성하면서 진행하고 있습니다. 그러다 보니 진정한 테스트 가능한 앱 코드를 만들기 위해 기존 회사앱에 Dagger 2를 적용시켜야할 필요성이 생겼습니다. 그런데 제가 오픈 소스를 적용하면 귀찮은 일을 해야 합니다. 왜 이 오픈 소스를 적용해야 하는지 팀원들한테 설득시켜야 하는데요. 처음 입사해서 몇번 했는데 제 시간 쏟으면서 발표자료 만들랴 일도 하랴 너무 귀찮습니다. 그리고 이번에 인사 평가가 나왔는데 기대한 것보다 실망스러워서 회사에 대한 애정도 없어진 터라 더 이상 발표 같은건 아예 안할 작정입니다;; 아무튼 Injector가 필요한데 Dagger 2를 앱에 녹이면 귀찮은 일이 생길것 같아서 아주 단순한 Injector를 만들어야 겠습니다. 앱 뿐만 아니라 SDK를 만들때도 이 단순한 Injector를 사용하면 괜찮겠다는 생각이 드네요. 


생각해보니 SDK 개발할 때 Dagger 2를 사용해도 될지 의문이 들고 아주 간단한 Injector를 하나 만들어서 두고두고 써 먹으면 제 실력도 향상되고 재밌을것 같습니다. 그래서 JSR 330(https://jcp.org/en/jsr/detail?id=330)을 준수하는 Injector를 만들까 합니다. GitHub 페이지만 만들었네요. https://github.com/jayjaylab/syringe 별생각없이 주입하는거니까 syringe라고 이름지었습니다. 시작을 해야 하는데 이것저것 하느라 진행을 못하고 있네요;;

Posted by 제이제이랩
,