[Android] Koin의 Service Locator 패턴
Android DI 하면 그다음 항상 따라오는 Dagger, Hilt, Koin 삼 형제가 있다.
여기서 우리가 귀가 아프도록 들었던 "Koin"
우리에게 익숙한 친구인 "Koin"이 사실은 DI가 아니라 Service Locator 패턴이라고 한다.
그럼 Service Locator란 무엇일까?
Service Locator란?
"서비스 로케이터 패턴은 로케이터에 객체의 초기화 방법을 등록하고, 해당 객체를 필요로 하는 곳에서 로케이터를 통해 객체를 제공받을 수 있도록 하는 패턴이다."
Service Locator는 중앙 레지스트리 같은 역할을 한다.
모든 인터페이스에 대한 구현체를 저장하고 기억하고 있다가 필요한 부분에서 사용하게 도와준다.
이와 같은 방식으로 클라이언트는 구현체와 더 느슨한 관계를 가지게 된다.
하지만 Service Locator는 싱글톤과 같은 형태로 구현체들과 관계가 형성된다.


하지만, Service Locator 패턴의 주요 문제점 중 하나는 클래스의 의존성을 명확하게 드러내지 않고 숨긴다는 점이다.
즉, 컴파일 시점에는 의존성 주입의 문제를 감지할 수 없지만, 런타임에서 오류가 발생할 가능성이 높아진다.
이는 Service Locator가 동적으로 의존성을 관리하기 때문이다. 모든 객체의 생성과 등록 과정이 런타임에서 이루어지므로, 특정 의존성이 누락되었거나 잘못된 구현체가 등록되었을 경우에도 컴파일러는 이를 감지할 수 없다.
따라서, 런타임에서 오류가 발생했을 때 이를 해결하려면, 해당 클래스가 어떤 의존성을 필요로 하는지 직접 확인하고, 해당 구현체가 Service Locator에 올바르게 등록되었는지를 파악해야 하는 어려움이 따른다.
이러한 문제로 인해 Service Locator는 코드의 가독성과 유지보수성을 저하시킬 가능성이 있으며, 의존성을 명확하게 드러내는 의존성 주입(Dependency Injection, DI) 방식이 더 권장되는 경우가 많다.
실제 Koin을 사용하는 (샘플) 프로젝트
아래의 코드처럼 module 블럭 안에서 객체 생성을 정의해준다. single은 module 내 하나의 인스턴스를, factory는 필요로 할 때 마다 새로운 인스턴스를 반환한다.
// koin module 정의
val appModule = module {
// single instance of SampleRepository
single<SampleRepository> { SampleRepositoryImpl()}
// Sample Presenter Factory
factory { SamplePresenter(get()) }
}
간단하게 2가지 Presenter를 만들어준다.
class SamplePresenter(val repo: SampleRepository) {
fun sayHello() = "${repo.giveHello()} from $this"
}class AnotherSamplePresenter(val repo: SampleRepository) {
fun sayHello() = "${repo.giveHello()} from $this"
}
AnotherSamplePresenter의 경우에는 module에 선언해주지 않았지만, SamplePresenter를 대체해도 컴파일 후 실행된다.
class MainActivity : AppCompatActivity() {
// [1]
val presenter by inject<SamplePresenter>() // [2]
val presenter by inject<AnotherSamplePresenter>()
//...
}
당연하지만 실행하면 [2] 의 부분에서 아래와 같은 에러가 발생한다.
Caused by: org.koin.core.error.NoBeanDefFoundException: No definition found for class:'com.example.koinsample.AnotherSamplePresenter'. Check your definitions!
코인이 런타임 에러가 발생한다는 점을 이해하려면 서비스 로케이터 패턴을 먼저 파악해야 한다.
앞으로 다룰 내용은 왜 DI와 서비스 로케이터 패턴이 필요했는지, DI와 서비스 로케이터는 무엇인지에 대해 직접 구현하는 과정이다.
예제를 통해 DI와 서비스 로케이터 패턴이 어떻게 쓰이는지 보고나면 결론을 이해하기 쉬울 것 같다.

샘플 컴포넌트 코드로 특정 감독의 영화 목록을 제공한다.
MovieLister 는 알고 있는 모든 영화를 반환하기 위해 Finder 객체를 요청한다.
다른 부분은 지금 다루는 문제가 아니라서 수정하지 않을 것이고, 이 글에서는 Finder 객체, 특히 Lister 객체를 특정 Finder 객체와 연결하는 방법이다.
이 부분에 집중한 이유는 moviesDirectedBy() 메서드가 모든 영화가 저장되는 방식과 완전히 독립적 이어질 수 있다.
그래서모든 메서드는 Finder를 참조하고 Finder가 하는 일은 findAll() 메서드에 응답하는 방법을 아는 것 뿐이다. Finder에 대한 인터페이스는 다음과 같다.

Finder 를 구현하고 있는 구체 클래스 객체는 Lister 의 생성자에서 초기화 해준다.

구체 클래스의 이름에서 확인할 수 있듯, source는 colon이 delimiter로 사용된 text 파일이다.
SQL 데이터베이스, XML파일, Web Service 등 다른 형식을 갖는 경우 해당 데이터를 가져오기 위해 다른 Finder 클래스가 필요하다. MovieFinder 인터페이스를 정의했기에 moviesDirectedBy() 메서드는 변경하지 않는다. 그러나 올바른 Finder 객체를 설정하기 위한 부분에는 수정이 필요하다.

사용자가 어떤 Finder를 사용할 지 모르는 상황이라 Finder의 구체 클래스는 컴파일 타임에 알 수 없게 만들어야 한다.
그렇게 하기 위해 제어의 역전(Inversion of Control)을 사용한다.
제어의 역전(Inversion of Control)
제어의 어떤 측면이 역전된 것일까?
위의 예제와 같은 상황에서 MovieLister는 단순히 MovieFinder 인터페이스로 데이터를 얻어올 수 있기에, MovieFinder를 구현한 구체 클래스의 인스턴스를 제공하는 외부 모듈이 필요하다.
이 외부 모듈을 지칭하기에 IoC는 너무 일반적인 용어여서 외부 모듈의 패턴과 이름에 대한 논의가 진행되었고, 그렇게 정해진 이름이 Dependency Injection이다.
추가로 Dependecy Injection이 MovieLister 클래스 내에서 MovieFinder 구체 클래스에 대한 의존성을 제거하기 위한 유일한 방법이 아니고, 대체할 수 있는 다른 패턴이 서비스 로케이터 패턴이다.
Dependency Injection Pattern

기본 개념은 별도의 객체인 Assembler가 MovieFinder 인터페이스를 구현한 구체 클래스를 갖는 것이다.
아래 세 가지 방법으로 주입받을 수 있다.
type 1 IoC — interface injection


type 2 IoC — setter injection

type 3 IoC — constructor injection

구체 클래스의 inject 과정
1. 구체 클래스와 연결해주기 위해 Configuration 을 정의해준다.
Container에 Configuration 정보인 Component와 Injector의 등록을 진행한다.

2. 컴포넌트를 등록해서 어떤 컴포넌트를 제공해주는지 Look Up Table(조회 테이블) 역할을 하도록 한다.
registerComponents()는 특정 클래스(MovieLister, ColonMovieFinder)를 컨테이너에 등록하여, 필요할 때 lookup()을 통해 가져올 수 있도록 한다.

3. 주입 가능한 클래스는 Injector를 구현해야한다.
의존성을 주입받을 클래스(MovieFinder, FinderFilenameInjector )는 Injector 인터페이스를 구현해야 한다.

4. Injector 를 구현하는 클래스는 inject() 메서드를 통해 target에 호출 객체 혹은 실제 데이터를 inject 해준다.
registerInjectors()를 통해 Injector를 Container에 등록한다.
그렇게하면, 이후 컨테이너가 종속성을 파악하여 Inector를 사용해 자동으로 주입할것이다.

5. 종속성이 잘 주입되었는지 파악

Service Locator Pattern
DI를 고안했던 목적인 MovieLister가 MovieFinder 의 구체 클래스에 의존하지 않도록 하는 것은 지금부터 설명할 서비스 로케이터 패턴으로도 만족된다.

서비스 로케이터는 응용 프로그램에서 필요한 모든 서비스를 확보하는 방법을 알고있는 객체이다.
- MovieLister는 ServiceLocator를 사용하여 MovieFinder 객체를 가져온다.
- 이를 통해 MovieLister는 MovieFinder의 구체적인 구현에 의존하지 않게 된다.
#1 Service Locator의 간단한 구현
1. ServiceLocator에서 movieFinder 객체를 가져온다

2. DI 에서처럼 ServiceLocator의 configuration을 설정한다.
configure()에서 ServiceLocator.load()를 호출하여 ServiceLocator 객체를 생성하고 MovieFinder를 설정.

3. MovieLister 내부에서 ServiceLocator 싱글톤 객체를 통해 MovieFinder 객체를 얻어오게 되어있다. 따라서 MovieLister의 사용 코드는 아래와 같다.

MovieLister는 내부적으로 ServiceLocator.movieFinder()를 호출하여 MovieFinder 객체를 사용할 수 있게 된다.
하지만, 이 구현에서의 문제점은 MovieLister는 하나의 서비스만 사용하지만 모든 서비스가 등록된 서비스 로케이터 객체 의존한다는 점이다. MovieListener는 MovieFinder 하나만 필요하지만, 이외 불필요한 서비스까지 ServiceLocator에 의존하게 되며, 이는 단일 책임(SRP)에 위배하게 된다.
이를 해결하기위해 로케이터 인터페이스를 분리하여 필요한 서비스 로케이터 객체에만 의존하도록 한다.
#2 Locator 인터페이스 분리
아래와 같이 인터페이스를 만들어 MovieLister가 전체 서비스 로케이터 인터페이스 대신 필요로 하는 인터페이스만 선언할 수 있다.


#1의 간단한 구현에서 static 메서드를 사용해서 ServiceLocator 객체를 가져오던 부분을 더이상 사용할 수 없게 되었다. MovieFinderLocator 객체를 얻기 위해서 래퍼 클래스를 사용해야 하며, MovieLister 에서 필요한 MovieFinder를 얻기 위해서 이 래퍼 클래스를 사용해야 한다.
서비스 로케이터 클래스에는 필요한 각 서비스에 대한 메서드가 있다는 점에서 이 방법은 정적(static)이다. 필요한 서비스를 숨기고 런타임에 선택할 수 있는 동적 서비스 로케이터를 만들어보자.
#3 동적 서비스 로케이터
기존 방식은 정적인 서비스 제공 방식이었다.
하지만 특정 서비스를 런타임에서 선택할 수 있도록 하기 위해 동적 서비스 로케이터를 사용할 수도 있다.
서비스 로케이터는 각 서비스에 대한 필드 대신 맵을 사용하고, 서비스를 가져오고 로드하는 일반적인 방법을 제공한다.



“DI” vs “Service Locator”
가시적인 차이점은 서비스 로케이터는 서비스를 사용하는 모든 부분에서 로케이터에 대한 의존성을 갖는다는 점이다. 로케이터는 다른 구현체에 대한 의존성을 감출 수 있지만 반드시 로케이터를 알아야한다.
따라서 이 부분에 대해서는 로케이터와 인젝터는 해당 종속성이 문제인지 여부에 따라 선택할 수 있다.
또 다른 차이점은 DI를 사용하면 컴포넌트의 의존성을 쉽게 파악할 수 있다. DI를 사용하면 생성자와 같은 Injection 방법을 보고 종속성을 알 수 있다. 서비스 로케이터를 사용하면 로케이터 호출에 대한 소스코드를 찾아야 한다.
Service Locator 패턴은 안티패턴인가?
서비스 로케이터 패턴이 적용된 프로젝트에서는 제공될 클래스를 반드시 생성해야 하고, 생성 후 로케이터에 등록해주는 과정이 필요하다. 이 부분이 유지보수 입장에서는 모든 코드를 파악하고 어디서 객체를 생성해서 등록해주고 있는지를 알아야 기능의 확장이 가능하다. 자세한 내용은 여기에서 확인이 가능하다.
안티-패턴인 이유는 아래 두가지 이유이다.
SOLID 원칙인 ISP(인터페이스 분리 원칙) 위반
ISP —사용하지 않는 메서드에 대한 의존이 강제되면 안 된다.
예를 들어, a(), b(), c()를 가지는 IA 인터페이스를 구현하는 구체 클래스 Foo와 Bar 가 있을 때 클래스 Foo에서는 a(), b()만 필요하고, 클래스 Bar에서는 a(), c()만 필요하다면 IA인터페이스는 a()만 가지고 IA의 하위 인터페이스인 IB, IC를 정의하여 각 각 b(), c()를 가지도록 분리되어야 한다는 원칙이다. 이 원칙은 하나의 클래스는 하나의 책임을 가져야 한다는 SRP를 지키는 것을 돕는다.
서비스 로케이터 패턴에서 로케이터는 동적으로 등록되는 경우 거의 무한한 메서드를 노출하고, 로케이터를 사용하는 곳에서 사용하지 않는 무한한 메서드에 의존해야 한다.
캡슐화 위반
캡슐화는 파라미터와 반환값이 명확하게 정의 되어 서로 상호작용 할 때 부족한 정보가 없어야 한다.
즉, 파라미터(사전 조건)와 반환값(사후 조건)이 명확해야 한다는 의미이다.
하지만 Service Locator 패턴에서는 이런 계약이 불분명하다.
동적 서비스 로케이터 구현에서 봤듯이 로케이터에 MovieFinder가 등록되어 있는 상황이라 “가정”하고 MovieLister에서 로케이터를 사용한다. MovieLister에서 로케이터를 사용할 당시에 로케이터 내에 MovieFinder가 등록되어 있다는 것을 컴파일 타임에는 보장할 수 없기 때문에 캡슐화 되었다고 보기 어렵다.
결론,
서비스 로케이터 패턴은 로케이터라는 싱글톤 객체내에 미리 정의한 방법으로 객체를 생성하게 되는데 로케이터를 사용하는 모든 클래스에서 로케이터에 등록된 객체를 안다는 점에서 SOLID 원칙의 ISP에 위배되고, 로케이터를 사용하면 명확한 파라미터와 반환값을 정의하지 않기 때문에 캡슐화에 위배된다.
이런 부분에서 안티-패턴이라는 의견이 있고, 사용하게 되면 런타임 에러 발생 가능성이 있다.
출처
DI 라이브러리 “Koin” 은 DI가 맞을까?
Dependency Injection Pattern 과 Service Locator Pattern에 대해서
dev-kimji1.medium.com
https://salmonpack.tistory.com/47
[Kotlin] Koin은 DI가 아니다?!
최근에 흥미로운 글 하나를 읽었습니다. 🧑💻🧑💻🧑💻 "Koin은 DI 인가 Service Locator 인가?" Android DI 하면 그다음 항상 따라오는 Dagger, Hilt, Koin 삼 형제가 있죠 여기서 우리가 귀가 아프도
salmonpack.tistory.com