|

의존성 주입과 서비스 로케이터 차이: 둘 다 객체를 꺼내 쓰는데 왜 평가가 다를까

의존성 주입과 서비스 로케이터 차이를 객체 책임 위치 관점으로 설명하는 대표 이미지
둘 다 결합을 줄이려는 시도일 수 있지만, 의존성을 누가 찾는지가 다르다

의존성 주입과 서비스 로케이터 차이는 겉으로 보면 작아 보입니다. 둘 다 직접 new를 줄일 수 있고, 둘 다 구현 교체를 쉽게 만들려는 시도처럼 보이기 때문입니다. 하지만 실제로는 중요한 차이가 있습니다. DI는 객체가 필요한 의존성을 외부에서 받는 방식이고, 서비스 로케이터는 객체가 필요한 의존성을 직접 찾는 방식입니다.

의존성 주입과 서비스 로케이터 차이를 비교한 요약 카드
먼저 큰 차이를 한눈에 잡는 요약 카드

왜 둘이 비슷해 보일까

둘 다 클래스가 구체 구현체에 직접 묶이는 문제, 구현 교체가 어려운 문제, 테스트에서 진짜 외부 객체를 끌고 와야 하는 문제를 줄이려는 방향으로 보이기 때문입니다. 하지만 더 중요한 질문은 객체가 협력 객체를 받는가, 아니면 직접 찾는가입니다.


DI는 무엇인가

Spring 공식 문서는 DI를 객체가 생성자 인자, 팩토리 메서드 인자, 또는 프로퍼티로 의존성을 제공받는 구조로 설명합니다. 쉽게 말하면 객체가 필요한 의존성을 스스로 만들거나 찾지 않고 외부에서 전달받는 것입니다.

interface PaymentGateway {
    void approve(int amount);
}

interface Notifier {
    void send(String message);
}

class OrderService {
    private final PaymentGateway paymentGateway;
    private final Notifier notifier;

    public OrderService(PaymentGateway paymentGateway, Notifier notifier) {
        this.paymentGateway = paymentGateway;
        this.notifier = notifier;
    }

    public void placeOrder(int amount) {
        paymentGateway.approve(amount);
        notifier.send("주문이 완료되었습니다.");
    }
}

OrderService는 무엇이 필요한지만 드러냅니다. 어떤 구현체를 쓸지와 어떻게 연결할지는 바깥 조립 지점의 책임입니다. 즉 DI의 핵심은 단순히 new를 감추는 것이 아니라 생성과 사용을 분리하는 것입니다.


서비스 로케이터는 무엇인가

서비스 로케이터에서는 객체가 외부에서 의존성을 전달받는 대신, 필요할 때 로케이터에게 가서 직접 가져옵니다. 이 방식에서도 구체 구현체를 직접 new 하지 않을 수는 있지만, 자기 의존성을 직접 찾는 책임은 객체 안에 남아 있습니다.

class ServiceLocator {
    public static PaymentGateway paymentGateway() {
        return new KakaoPayGateway();
    }

    public static Notifier notifier() {
        return new EmailNotifier();
    }
}

class OrderService {
    private final PaymentGateway paymentGateway;
    private final Notifier notifier;

    public OrderService() {
        this.paymentGateway = ServiceLocator.paymentGateway();
        this.notifier = ServiceLocator.notifier();
    }

    public void placeOrder(int amount) {
        paymentGateway.approve(amount);
        notifier.send("주문이 완료되었습니다.");
    }
}

생성자만 봐서는 OrderService가 무엇을 필요로 하는지 잘 드러나지 않습니다. 내부 구현을 열어봐야 알 수 있습니다. DI는 의존성을 바깥으로 드러내고, 서비스 로케이터는 의존성 조회를 객체 안에 남깁니다.


왜 DI 쪽이 더 선호될까

Microsoft Learn은 하드코딩된 의존성이 구현 교체를 어렵게 하고, 의존성 설정을 흩어지게 만들고, 단위 테스트를 어렵게 만든다고 설명합니다. DI는 이 문제를 구조적으로 줄여줍니다.

  1. 테스트 대역을 넣기 쉽다
  2. 필수 의존성이 생성자 시그니처에 드러난다
  3. 조립 책임을 메인 함수나 설정 지점에 모으기 쉽다
class FakePaymentGateway implements PaymentGateway {
    private int approvedAmount;

    @Override
    public void approve(int amount) {
        this.approvedAmount = amount;
    }
}

class FakeNotifier implements Notifier {
    @Override
    public void send(String message) {
    }
}

OrderService service = new OrderService(
    new FakePaymentGateway(),
    new FakeNotifier()
);

이렇게 하면 서비스 로직만 가볍게 검증할 수 있습니다. 테스트가 쉬워진다는 말은 mock 프레임워크를 쓰기 좋다는 뜻보다, 핵심 로직을 바깥 기술에서 분리해 검증하기 쉬워진다는 뜻에 더 가깝습니다.


그렇다면 서비스 로케이터는 항상 나쁜가

과장하지 않는 편이 좋습니다. 서비스 로케이터를 절대 악처럼 말하면 오히려 감각이 흐려집니다. 핵심은 의존성이 객체 외부에 드러나는가, 내부 조회로 숨겨지는가입니다. 특정 레거시 구조나 플러그인 환경에서는 로케이터나 레지스트리 형태가 실용적으로 쓰일 수도 있습니다.


DI 컨테이너는 이 이야기에서 어디에 있나

DI는 설계 방식이고, 컨테이너는 그 방식을 자동화하는 도구입니다. 메인 함수에서 직접 구현체를 만들어 생성자에 넣어도 DI는 이미 성립합니다.

public class Main {
    public static void main(String[] args) {
        PaymentGateway paymentGateway = new KakaoPayGateway();
        Notifier notifier = new EmailNotifier();
        OrderService orderService = new OrderService(paymentGateway, notifier);

        orderService.placeOrder(15000);
    }
}

즉 비교는 이렇게 기억하면 깔끔합니다. DI는 의존성을 외부에서 받는 방식, Service Locator는 객체가 필요한 의존성을 직접 찾는 방식, DI 컨테이너는 DI 조립을 자동화하는 도구입니다.


의존성 주입과 서비스 로케이터 차이를 실무에서 판단하는 기준

  • 필수 의존성을 코드에서 선명하게 드러내고 싶다
  • 테스트 대역 주입 이점이 크다
  • 객체가 구현 선택 책임까지 떠안지 않게 하고 싶다
  • 외부 연동·저장소·알림처럼 교체 가능성이 있다

반대로 생성자만 봐서는 필요한 의존성이 잘 안 보이고, 객체가 로케이터를 여러 번 호출하고, new는 사라졌지만 책임 분리는 생각보다 크게 줄지 않았다면 서비스 로케이터 냄새가 나는 구조일 수 있습니다.


자주 하는 오해

  1. new가 없으면 다 DI다
  2. 컨테이너를 쓰면 자동으로 좋은 설계다
  3. 작은 코드도 무조건 인터페이스와 DI로 감싸야 한다

세 가지 모두 조심해야 합니다. 중요한 것은 구조가 멋져 보이느냐가 아니라, 그 분리가 실제로 변경 비용과 테스트 비용을 낮추느냐입니다.


마무리

의존성 주입과 서비스 로케이터 차이의 핵심은 둘 다 직접 new를 줄이느냐가 아닙니다. 핵심은 객체가 의존성을 받는가, 아니면 직접 찾는가입니다.

같이 읽으면 좋은 글은 인터페이스는 왜 필요할까, 좋은 객체지향 설계는 의존성을 어떻게 다룰까, SOLID 원칙, 실무에서는 어떻게 봐야 할까입니다.

함께보면 좋은 글