한 줄 정의
객체가 의존하는 서비스를 직접 생성하지 않고, 중앙 레지스트리(서비스 로케이터) 에 요청해서 받아오는 디자인 패턴입니다.
쉽게 말하면
큰 회사 빌딩에 처음 방문했을 때 “회계팀 어디예요?”라고 물어보면 안내 데스크 직원이 위치를 알려줍니다. 방문자는 회계팀이 몇 층 어느 자리에 있는지 외우지 않아도 되고, 회계팀이 자리를 옮겨도 안내 데스크만 갱신하면 됩니다.
서비스 로케이터가 바로 이 안내 데스크입니다. 객체는 “PaymentGateway 좀 줘”라고만 말하고, 어떤 구현체인지·어디에 있는지·어떻게 만드는지는 로케이터가 알아서 처리합니다.
왜 이렇게 설계했는가?
해결하는 문제
객체가 의존성을 직접 생성(new PaymentGateway())하면 두 가지가 박혀버립니다.
- 구체 클래스 선택: 어떤 구현체를 쓸지가 코드에 하드코딩됩니다.
- 생성 책임: 의존성의 생성자 시그니처가 바뀌면 사용하는 모든 곳이 영향받습니다.
서비스 로케이터는 이 두 가지를 레지스트리 라는 한 곳으로 옮겨, 사용자 코드는 “인터페이스만 안다” 는 상태를 유지하게 합니다.
이게 없다면
public class OrderService {
public void process() {
PaymentGateway gateway = new TossPaymentGateway(apiKey, ...);
gateway.charge();
}
}TossPaymentGateway 를 KakaoPaymentGateway 로 바꾸려면 OrderService 코드를 직접 수정해야 합니다. 테스트할 때 mock으로 갈아끼우기도 어렵습니다. 이런 결합을 떼어내려는 시도가 서비스 로케이터와 의존성 주입의 출발점입니다.
작동 방식
핵심 구성 요소
flowchart LR Client[클라이언트] -->|"get(PaymentGateway.class)"| Locator[Service Locator<br/>레지스트리] Locator -->|반환| Impl[구현체 인스턴스] Registrar[초기화 코드] -.->|"register(...)"| Locator
| 요소 | 책임 |
|---|---|
| Service Locator | 서비스 인스턴스를 보관하고 키로 조회 가능하게 하는 레지스트리 |
| Service Interface | 클라이언트가 의존하는 추상 타입 |
| Service Implementation | 실제 구현. 부팅 시점에 로케이터에 등록됨 |
| Client | 로케이터에 요청해 서비스를 얻어 사용 |
최소 구현
public class ServiceLocator {
private static final Map<Class<?>, Object> services = new HashMap<>();
public static <T> void register(Class<T> type, T instance) {
services.put(type, instance);
}
@SuppressWarnings("unchecked")
public static <T> T get(Class<T> type) {
T service = (T) services.get(type);
if (service == null) {
throw new IllegalStateException("No service for " + type);
}
return service;
}
}
// 부팅 시점
ServiceLocator.register(PaymentGateway.class, new TossPaymentGateway(...));
// 사용 측
public class OrderService {
public void process() {
PaymentGateway gateway = ServiceLocator.get(PaymentGateway.class);
gateway.charge();
}
}동적 등록 (Lazy / Factory 기반)
서비스 인스턴스를 미리 만들기 비싸면, 팩토리 를 등록해 두고 필요할 때 생성합니다. Spring BeanFactory 가 사실상 이 형태입니다.
원격 위치 조회
분산 환경에서는 서비스 인스턴스 자체가 아니라 서비스의 위치(URL/큐 이름) 를 반환합니다. 이게 마이크로서비스의 서비스 디스커버리(Service Discovery)와 같은 개념입니다.
// 로컬 객체 반환
ServiceLocator.get(PaymentGateway.class) → TossPaymentGateway 인스턴스
// 원격 엔드포인트 반환
ServiceLocator.get("PaymentService") → "https://payment-service:8080"의존성 주입(DI)과의 비교
서비스 로케이터와 DI는 같은 문제(의존성 결합 분리) 를 푸는 두 가지 다른 답입니다. 자주 헷갈리지만 제어의 방향이 정반대입니다.
| 구분 | 서비스 로케이터 | 의존성 주입 (DI) |
|---|---|---|
| 의존성 획득 방향 | 클라이언트가 끌어옴 (pull) | 외부에서 밀어 넣음 (push) |
| 의존성이 명시되는 곳 | 메서드 본문 | 생성자/세터/필드 |
| 컴파일러가 누락 의존성 감지 | 못 함 (런타임 에러) | 잡아냄 (컴파일/주입 시점) |
| 테스트 용이성 | 글로벌 로케이터 mock 필요 | 생성자에 mock 직접 주입 |
| 클래스 의존성 가시성 | 코드를 읽어야 보임 | 시그니처만 봐도 보임 |
| 결합도 | 로케이터에 의존 | 인터페이스에만 의존 |
코드로 보는 차이
// 서비스 로케이터: "필요할 때 데려옴"
public class OrderService {
public void process() {
var gateway = ServiceLocator.get(PaymentGateway.class);
var notifier = ServiceLocator.get(Notifier.class);
gateway.charge();
notifier.send();
}
}
// DI: "처음에 받아 둠"
public class OrderService {
private final PaymentGateway gateway;
private final Notifier notifier;
public OrderService(PaymentGateway gateway, Notifier notifier) {
this.gateway = gateway;
this.notifier = notifier;
}
public void process() {
gateway.charge();
notifier.send();
}
}DI 버전은 생성자만 봐도 OrderService 가 두 가지에 의존한다는 사실 이 드러납니다. 서비스 로케이터 버전은 메서드 본문을 다 읽어야 알 수 있습니다.
안티패턴 논쟁
Mark Seemann의 비판
Dependency Injection in .NET 의 저자 Mark Seemann은 서비스 로케이터를 안티패턴 으로 분류했습니다. 핵심 이유는 의존성이 숨겨지기 때문 입니다.
public class OrderService {
public OrderService() { } // 깔끔해 보이지만...
public void process() {
// 사실 이 클래스는 PaymentGateway, Notifier, Logger,
// OrderRepository, EventBus 등에 의존합니다.
// 생성자만 보면 전혀 알 수 없습니다.
}
}이 문제는 다음 결과로 이어집니다.
- 재사용성 저하: 다른 프로젝트에 이 클래스를 가져가려면 그 프로젝트의 로케이터에도 모든 의존성을 등록해야 함
- 테스트 어려움: 단위 테스트 작성 시 글로벌 상태(로케이터)를 깨끗이 셋업/리셋해야 함
- 런타임 폭발: 의존성 누락이 컴파일이 아닌 운영 환경 첫 호출에서 NPE로 터짐
- 암묵적 결합: “이 메서드를 호출하면 어떤 서비스가 필요한지” 호출자가 알 수 없음
Martin Fowler의 중립 평가
Martin Fowler는 Inversion of Control Containers and the Dependency Injection pattern에서 두 패턴을 대등한 IoC 구현 방식 으로 소개했습니다. 단, “DI를 더 선호한다”는 입장을 밝혔습니다.
정리
| 상황 | 권장 |
|---|---|
| 일반적인 애플리케이션 코드 | DI 우선 |
| 프레임워크/라이브러리 내부에서 동적 컴포넌트 디스패치 | 서비스 로케이터 허용 |
| 레거시 코드의 점진적 리팩터링 | 서비스 로케이터 → 단계적으로 DI로 |
| 분산 환경에서 원격 서비스 위치 조회 | 서비스 로케이터(=서비스 디스커버리)가 정답 |
핵심
서비스 로케이터는 객체 의존성 에 쓰면 안티패턴이지만, 분산 시스템의 위치 조회 에 쓰면 표준 패턴입니다. 같은 이름의 두 다른 도구라고 봐도 됩니다.
실무 사례
Spring ApplicationContext 를 로케이터로 쓰기 (안티패턴)
@Component
public class OrderService implements ApplicationContextAware {
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext context) {
this.context = context;
}
public void process() {
PaymentGateway gateway = context.getBean(PaymentGateway.class);
gateway.charge();
}
}스프링 자체는 DI 컨테이너지만, ApplicationContextAware 를 통해 getBean() 을 호출하는 순간 서비스 로케이터로 타락 합니다. 보통은 안티패턴으로 보고, 순환 의존성 회피 같은 정말 어쩔 수 없는 경우에만 허용합니다.
마이크로서비스 디스커버리 (정상적 사용)
| 도구 | 역할 |
|---|---|
| Eureka | 넷플릭스 OSS의 서비스 레지스트리. 서비스가 자신을 등록하고, 클라이언트가 조회 |
| Consul | HashiCorp. 헬스체크와 KV 스토어까지 제공 |
| Kubernetes Service | DNS 기반 서비스 디스커버리. payment-service:8080 같은 이름으로 조회 |
| Spring Cloud LoadBalancer | Eureka/Consul과 결합해 로드밸런싱까지 처리 |
이 도구들이 하는 일이 정확히 서비스 로케이터입니다. 마이크로서비스 환경에서는 피할 방법이 없는 패턴 입니다.
플러그인 시스템
마이크로커널 아키텍처의 플러그인 레지스트리 가 서비스 로케이터의 전형입니다. 코어가 registry.get("iPhone6s") 로 플러그인을 찾아 호출합니다. 이 경우는 안티패턴이 아니라 아키텍처 자체의 핵심 메커니즘 입니다.
서비스 기반 아키텍처에서
서비스 기반 아키텍처에서는 UI가 도메인 서비스를 호출할 때 서비스 로케이터로 위치를 찾아갑니다. 보통 API 게이트웨이나 프록시에 로케이터 기능을 내장합니다.
적합한 상황
사용하면 좋은 경우
- 분산 시스템의 서비스 위치 조회: Eureka·Consul·K8s DNS 등 거의 강제됨
- 플러그인/확장 가능한 아키텍처: 동적 디스패치가 본질인 시스템
- 레거시 코드에 DI 컨테이너 도입 전 임시 단계: 점진적 리팩터링의 디딤돌
사용하지 말아야 할 경우
- 일반 비즈니스 로직 클래스의 의존성 처리: DI를 써야 함
- 단위 테스트가 중요한 코드: 글로벌 상태 때문에 테스트 격리가 어려워짐
- 의존성을 명시적으로 드러내야 하는 도메인 모델
내 생각
- 한국 실무에서는 Spring을 쓰면서도
ApplicationContext.getBean()을 너무 쉽게 부르는 코드를 종종 봅니다. 대부분은 순환 의존성을 회피하려는 임시방편 인데, 진짜 해결은 의존성 그래프를 다시 그리는 것이지 로케이터로 숨기는 게 아닙니다. 로케이터가 늘어나는 코드베이스는 의존성 그래프가 망가지고 있다는 신호입니다. - 서비스 로케이터를 “안티패턴”이라고 한 줄로 정리해 버리는 건 위험합니다. 객체 단위 의존성에선 안티패턴, 분산 시스템 위치 조회에선 표준 패턴 이라는 두 맥락을 구분해야 합니다. Mark Seemann의 비판은 전자에 한정된 이야기입니다.
- DI vs 서비스 로케이터 논쟁은 사실 “의존성을 어디서 보이게 할 것인가” 의 문제입니다. DI는 생성자에서, 서비스 로케이터는 사용 시점에서. 가독성·테스트성·유지보수성 측면에서 생성자가 압도적으로 유리하기 때문에 일반적으로 DI가 정답입니다.
- 마이크로서비스의 서비스 디스커버리를 “서비스 로케이터 패턴”이라고 부르는 사람이 별로 없는 게 흥미롭습니다. 같은 패턴인데 분산 시스템 맥락에서는 그냥 “service discovery”라고 부르는 게 굳어졌습니다. 본질은 동일합니다.
더 알아볼 것
- Spring의
ObjectProvider<T>와Provider<T>— 서비스 로케이터의 타입 안전한 변형 - Eureka vs Consul vs Kubernetes DNS 디스커버리 비교
- 서비스 메시(Istio, Linkerd)가 서비스 로케이터를 어떻게 사이드카로 흡수하는지
- CDI(Contexts and Dependency Injection) 표준의
Instance<T>API - Java
ServiceLoaderAPI — JDK 표준 서비스 로케이터
관련 개념
출처
- Martin Fowler, “Inversion of Control Containers and the Dependency Injection pattern” (2004)
- Mark Seemann, Dependency Injection in .NET (2011)
- 마크 리처즈, 닐 포드, 『소프트웨어 아키텍처 The Basics』, 14장 서비스 기반 아키텍처