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) 모델이라는 점이다.

클린 코드(로버트.C.마틴 저)를 오랜만에 정주행 하고 좋은 구조를 디자인 하기 위해서 DIP(Dependency Inversion Principle)에 따라 구현체를 인터페이스에 주입하기 위해서 Guice(참고[1])를 사용하였다.

리뷰 알림의 Dependency Injection

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

review-notifier-class-diagram

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 exception) {
    throw constructLoadException(exception);
} catch (IllegalAccessException exception) {
    throw constructLoadException(exception);
}

다행히도 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 잘 써봐야겠다.


  1. 참고[1] Guice, Google
    https://github.com/google/guice ↩︎

  2. 참고[2] JavaDoc FXMLLoader
    https://docs.oracle.com/javase/8/javafx/api/javafx/fxml/FXMLLoader.html#load-java.net.URL-java.util.ResourceBundle-javafx.util.BuilderFactory-javafx.util.Callback- ↩︎