JavaFX는 Java 기반으로 Desktop Application 을 만드는 방법 중 하나입니다. 저의 첫 회사에서는 Web service 대신 Desktop application 에 대한 요구사항만 있어서 주로 사용하였습니다. .NET 이면 WinForm, Java면 JavaFX로 구현하였습니다. 지금은 Web service만 개발을 하다보니 사용할 일이 없다가 업무용 리뷰 알림 유틸리티를 MacOS용으로 만들기 위해서 오랜만에 사용하였습니다.

JavaFX의 특징은 .NET WPF(Windows Presentation Foundation)와 유사하게 View(FXML)과 Controller, Model이 명확하게 구분되는 MVC(Model-View-Controller) 모델이라는 점입니다. MVC를 기반으로 디자인하는 과정 중 클린 코드(로버트.C.마틴 저)를 오랜만에 정주행 하고 좋은 구조를 디자인 하기 위해서 DIP(Dependency Inversion Principle)에 따라 구현체를 인터페이스에 주입하기 위해서 Guice[참고1]를 사용하였습니다.

리뷰 알림의 Dependency Injection

리뷰 알림의 구조에서 의존성 주입이 필요한 부분은 다음과 같습니다.

리뷰알림서비스 클래스 다이어그램

JavaFX의 화면 별 Controller에서 ReviewNotifyService을 함수를 호출하여 ‘리뷰 목록’, ‘수동 조회 & 알림’ 등의 기능을 제공합니다. 그리고 리뷰알람을 제공하는 ReviewNotifyService는 ReviewNotifyModule(Guice, Dependency injection 설정 클래스)에 Singleton으로 GerritToSlackReviewWorkService 구현체로 정의하였습니다.

// ReviewNotifyModule.java
public class ReviewNotifierModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(ReviewWorkService.class)
                .to(GerritToSlackReviewWorkService.class)
                .in(Scopes.SINGLETON);
        bind(ReviewConfigRepository.class)
                .to(RocksDbReviewRepository.class);
        bind(ReviewNotifierRepository.class)
                .to(RocksDbReviewRepository.class);
    }
}

JavaFX에서 Guice 의존성 주입이 안되는 현상

JavaFX에서 Controller를 생성하는 코드에서 Guice의 인스턴스 함수를 호출하지 않아 의존성이 주입되지 않습니다.

Parent root = FXMLLoader.load(getClass().getResource("/fxml/Main.fxml"));                

위 코드의 문제점이 무엇인지 확인해보았습니다. [참고2]

FXMLLoader.load(URL location) 메소드는 callerClass를 입력하지 않으면 null 로 전달됩니다. 다음 코드와 같이 ReflectionUtil.newInstance를 사용해서 Guice의 instance 생성 메소드를 통하지 않기 때문에 @Inject Annotation이 반영되지 않는 현상입니다.

FXMLLoader.java의 posscessAttributes 메소드 중 일부

try {
    if (controllerFactory == null) {
        setController(ReflectUtil.newInstance(type));
    } else {
        setController(controllerFactory.call(type));
    }
} catch (InstantiationException e) {
    throw constructLoadException(e);
} catch (IllegalAccessException e) {
    throw constructLoadException(e);
}

다행히도 FXMLLoader에 controllerFactory를 입력받도록 load 메소드가 변경되어 이를 이용하면 Guice를 통한 (혹은 그 외의 의존성 라이브러리) 의존성 주입이 가능합니다.

JavaFX에서 Guice를 이용한 의존성 주입

생각보다 JavaFX에서 Guice의 의존성 주입을 사용하는 방법은 간단하였습니다. 과거 버전에서는 Provider를 정의하여 FXML에서 Node 인스턴스 생성 시 Guice injector의 getInstance() 함수를 이용하도록 설정하기도 하였습니다.

하지만 JDK8부터는 다음과 같이 injector의 getInstance 함수만 전달하여 Guice로 의존성 주입이 가능합니다.

Injector injector = Guice.createInjector(new ReviewNotifyModule());

Parent root = FXMLLoader.load(
        getClass().getResource("/fxml/Main.fxml"),
        null,
        new JavaFXBuilderFactory(),
        injector::getInstance);

Controller 사용 예제

private final ReviewWorkService reviewWorkService;

@Inject
public MainController(ReviewWorkService reviewWorkService) {
    this.reviewWorkService = reviewWorkService;
}

DIP를 준수하는 JavaFX 어플리케이션을 만들었습니다. 그래서 Guice를 통해서 객체 생성을 관리하니 편리하고 명확하여 좋았습니다.

Guice 음, 뭐, 의존성 용량이 생각보다 크다?

Guice를 적용하기 전 2MB(JRE 제외, MacOS 패키징, .app 파일) 하던 패키지가 적용 후엔 4MB로 증가하는 슬픈 사실을 발견하였습니다. 어차피 사내 내부적으로 소수만 사용할 토이 프로그램이기 때문에 상관은 없지만 예상치 못한 용량 증가였습니다. Guice에서 Guava까지 의존성을 이어서 가져가서 생긴 현상으로 사실 어느정도 복잡도를 가진 프로그램이라면 DIP 반영과 Guava의 편한 기능을 사용한다라고 생각하면 고민거리는 아닙니다.

SRP(Single responsibility Principle)와 DIP를 통한 명확한 의존성 분리와 주입이 더 좋은 코드를 만들 기회를 준다는 것에 공감하기 때문에 앞으로도 Guice 잘 써봐야겠습니다.