한 줄 정의

객체가 의존하는 서비스를 직접 생성하지 않고, 중앙 레지스트리(서비스 로케이터) 에 요청해서 받아오는 디자인 패턴입니다.

쉽게 말하면

큰 회사 빌딩에 처음 방문했을 때 “회계팀 어디예요?”라고 물어보면 안내 데스크 직원이 위치를 알려줍니다. 방문자는 회계팀이 몇 층 어느 자리에 있는지 외우지 않아도 되고, 회계팀이 자리를 옮겨도 안내 데스크만 갱신하면 됩니다.

서비스 로케이터가 바로 이 안내 데스크입니다. 객체는 “PaymentGateway 좀 줘”라고만 말하고, 어떤 구현체인지·어디에 있는지·어떻게 만드는지는 로케이터가 알아서 처리합니다.

왜 이렇게 설계했는가?

해결하는 문제

객체가 의존성을 직접 생성(new PaymentGateway())하면 두 가지가 박혀버립니다.

  1. 구체 클래스 선택: 어떤 구현체를 쓸지가 코드에 하드코딩됩니다.
  2. 생성 책임: 의존성의 생성자 시그니처가 바뀌면 사용하는 모든 곳이 영향받습니다.

서비스 로케이터는 이 두 가지를 레지스트리 라는 한 곳으로 옮겨, 사용자 코드는 “인터페이스만 안다” 는 상태를 유지하게 합니다.

이게 없다면
public class OrderService {
    public void process() {
        PaymentGateway gateway = new TossPaymentGateway(apiKey, ...);
        gateway.charge();
    }
}

TossPaymentGatewayKakaoPaymentGateway 로 바꾸려면 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의 서비스 레지스트리. 서비스가 자신을 등록하고, 클라이언트가 조회
ConsulHashiCorp. 헬스체크와 KV 스토어까지 제공
Kubernetes ServiceDNS 기반 서비스 디스커버리. payment-service:8080 같은 이름으로 조회
Spring Cloud LoadBalancerEureka/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 ServiceLoader API — JDK 표준 서비스 로케이터

관련 개념

출처

  • Martin Fowler, “Inversion of Control Containers and the Dependency Injection pattern” (2004)
  • Mark Seemann, Dependency Injection in .NET (2011)
  • 마크 리처즈, 닐 포드, 『소프트웨어 아키텍처 The Basics』, 14장 서비스 기반 아키텍처