배경
Java용 SPI .jar 파일을 build하여 다른 프로젝트에 dependency 형태로 추가하여 사용하는 작업이 필요했다. 구체적으로는 keycloak의 provider를 .jar로 개발해야하는 상황이였다. 순수 Java로 개발하는 경우, Spring의 의존성을 최대한 지양하는 것이 좋다. 다른 프로젝트
에서는 Java만을 통해 해당 .jar를 사용할 것이기 때문이다.
1. Factory Pattern
기존에 Spring을 사용할 때는 dependency injection을 위해 @Autowire 등을 사용하여 한 객체에서 다른 객체의 Bean을 참고하도록 처리했다. 그러나 Spring을 사용하지 않을 때는 직접 구현체를 넣으면 될 것이다. 예시를 들자면 아래와 같다.
public class TestServiceImpl implements TestService {
private final DependentService dependentService;
public TestServiceImpl() {
this.dependentService = new DependentServiceImpl();
}
DependentServiceImpl 구현체를 직접 TestServiceImpl의 생성자에 넣어주었다. 이렇게 해도 되지만 이런 구조는 다형성을 포기하는 구조가 된다. DepdentService를 사용하는 TestServiceImpl 같은 구현체가 여러 개라면, 그 모든 객체에서 DependentService의 구현체를 변경해야하는 상황에서 모두 변경을 해줘야한다.
따라서 Factory Pattern을 이용한다. 의존성의 주입 역할을 하는 객체를 Factory 객체로 만들고, Service Loader를 통해 인터페이스의 구현체가 지정되도록 IoC(Inversion of Control)를 적용해준다.
public class TestServiceImpl implements TestService {
private final DependentService dependentService;
public TestServiceImpl() {
this.dependentService = DependentServiceFactory.getInstance();
}
Factory용 코드는 다음과 같다. static 변수와 메서드를 선언해주고, instance를 지정한다.
public class DependentServiceFactory {
@Getter
private static final DependentService instance;
static {
ServiceLoader<ValidationService> loader = ServiceLoader.load(DependentService.class);
Optional<DependentService> dependentService = loader.findFirst();
instance = dependentService.orElseGet(DependentServiceImpl::new);
}
}
ServiceLoader에 DependentService 타입으로 등록된 구현체들을 찾아내서, 그 중 맨 처음 것을 지정하도록 했다. 나중에 변경이 필요하다면 이 부분만 변경하면 된다.
2. Service Loader
자바의 Service Loader는 특정 인터페이스를 구현하는 클래스를 동적으로 검색하고 로드할 수 있도록 해주는 기능이다. keycloak에서 정의하는 Provider 개념도, Spring에서 사용하는 Dependency Injection 개념도 이 기능을 기반으로 한다.
위 Factory 코드를 작성하고, 구현체를 Java가 인식하게 하기 위해서는 resources/META-INF/services 폴더에 서비스 클래스의 풀 패키지(FQCN, Full Qualified Class Name)를 적어줘야한다. 다시 말해 위 예제에서 DependentService.class의 class 위치를 적어주는 file을 생성해야한다. 예를 들어 com.example.test.services.DependentService 라는 이름의 파일을 만들어야한다.
그리고 해당 파일 내부에 실제 구현체의 FQCN을 적어주면 된다. 그럼 Service Loader가 인식한다.
3. @AutoService
구글의 Auto service 라이브러리를 이용하면 META-INF/services에 굳이 인터페이스-구현체간 관계를 정의하는 파일을 만들어주지 않아도 된다. 아래처럼 dependency를 추가한 후,
implementation 'com.google.auto.service:auto-service:1.1.1'
annotationProcessor 'com.google.auto.service:auto-service:1.0.1'
내가 사용하는 구현체에 interface명을 @AutoService 어노테이션과 함께 선언해주면 된다.
@AutoService(value = DependentService.class)
public class DependentServiceImpl implements DependentService {
'Programming-[Backend] > Java' 카테고리의 다른 글
[TIL] JVM HeapSize, HeapDumpPath 설정 (0) | 2025.01.06 |
---|---|
Virtual Thread 기초 (0) | 2024.12.08 |
비동기 작업: @Async 어노테이션, 스레드 관리 (0) | 2024.11.14 |
[TIL] Docker image JVM Heap 크기 및 옵션 설정, buildpack-gradle bootBuildImage, Packeto buildpack (0) | 2024.07.26 |
자바 기초 강의 정리 - 8. 클래스 패스, JAR (0) | 2024.07.15 |