Dependency Injection이란 무엇이며 Spring에서 어떻게 구현되는가?

발행: (2025년 12월 19일 오후 12:16 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

Source:

Introduction

새로운 TV를 구입했는데, 고장이 날 때마다 모든 부품이 단단히 용접되어 있어 직접 수리해야 한다고 상상해 보세요. 얼마나 답답한가요? 이제 각 부품을 독립적으로 교체할 수 있다면—유지보수가 훨씬 쉬워집니다.

이것이 바로 Java 프로그래밍에서 **의존성 주입(Dependency Injection, DI)**이 해결하는 문제입니다.

초보자들이 Java 코드를 작성할 때, 클래스가 스스로 의존성을 생성하는 경우가 많습니다. 이렇게 하면 애플리케이션을 테스트하기 어렵고, 변경하기도 힘들며, 결합도가 높아집니다. 스프링은 의존성 주입이라는 핵심 개념을 사용해 이를 우아하게 해결합니다.

이 글에서는 의존성 주입이 무엇인지, 왜 중요한지, 그리고 스프링이 이를 어떻게 구현하는지를 간단하고 실제적인 비유와 깔끔한 Java 21 예제로 배웁니다. 끝까지 읽으면 DI가 현대 Java 개발의 기반으로 여겨지는 이유를 이해하게 될 것입니다.

Source:

핵심 개념

의존성 주입이란?

간단히 말하면, 의존성 주입은 객체가 스스로 만들지 않고 필요한 것을 제공받는 것입니다.

비유: 모바일 충전기
휴대폰이 자체적으로 충전기를 만들지는 않습니다. 필요할 때 충전기를 주입합니다. 이를 통해:

  • 다양한 충전기 사용 가능
  • 손쉬운 교체
  • 더 큰 유연성

Java에서 의존성은 보통 해당 클래스가 동작하기 위해 필요한 다른 클래스를 의미합니다.

의존성 주입이 없는 경우 (강하게 결합된 코드)

public class Car {
    private Engine engine = new Engine(); // tightly coupled
}

문제점

  • 엔진을 쉽게 교체할 수 없음
  • 테스트가 어려움
  • 코드가 경직됨

의존성 주입이 적용된 경우 (느슨하게 결합된 코드)

public class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }
}

이제 Engine이 내부에서 생성되는 것이 아니라 주입되므로 유연성과 테스트 가능성이 향상됩니다.

스프링이 의존성 주입을 구현하는 방식

스프링은 제어 역전(IoC, Inversion of Control) 원칙을 사용합니다.

  • 필요한 객체를 정의합니다.
  • 스프링이 언제, 어떻게 객체를 생성할지 결정합니다.
  • 스프링이 자동으로 의존성을 주입합니다.

스프링은 DI를 다음과 같이 지원합니다:

  • 생성자 주입 (권장)
  • 세터 주입
  • 필드 주입 (비권장)

스프링에서 의존성 주입의 장점

  • 느슨한 결합
  • 테스트 용이 (모킹)
  • 유지보수성 향상
  • 더 깔끔하고 가독성 높은 코드
  • 엔터프라이즈 수준의 아키텍처

코드 예제 (Java 21)

예제 1: 생성자 주입 (권장 방식)

// Dependency
@Component
public class PaymentService {

    public String processPayment() {
        return "Payment processed successfully";
    }
}
// Dependent class
@Component
public class OrderService {

    private final PaymentService paymentService;

    // Constructor Injection
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public String placeOrder() {
        return paymentService.processPayment();
    }
}

이 방식이 좋은 이유

  • 의존성이 명시적입니다
  • 테스트가 쉽습니다
  • 불변이며 안전합니다

예제 2: 세터 주입 (선택적 의존성)

@Component
public class NotificationService {

    public void notifyUser() {
        System.out.println("User notified");
    }
}
@Component
public class UserService {

    private NotificationService notificationService;

    @Autowired
    public void setNotificationService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void registerUser() {
        notificationService.notifyUser();
    }
}

사용 시점

  • 의존성이 선택적이거나 구성 가능할 때 유용합니다.

  • 생성자 주입을 선호하세요 – 의존성을 명확히 하고 null 문제를 방지합니다.

  • 필드 주입을 피하세요 – 의존성을 숨기고 테스트를 어렵게 합니다.

  • 구현이 아니라 인터페이스에 의존하세요 – 유연성과 테스트 가능성을 향상시킵니다.

  • 빈을 집중화하세요 – 하나의 클래스는 하나의 책임만 가져야 합니다.

  • Spring이 객체 생성을 관리하도록 하세요 – Spring‑관리 빈에 new 사용을 피합니다.

피해야 할 일반적인 실수

  • 수동 객체 생성을 Spring DI와 혼합하기.
  • @Autowired를 불필요하게 과도하게 사용하기.
  • 순환 의존성 만들기.
  • 서비스 설계에서 인터페이스를 무시하기.

Conclusion

Spring에서의 의존성 주입은 단순히 프레임워크 기능이 아니라, Java 애플리케이션을 깔끔하고 유연하며 유지보수하기 쉽게 만드는 설계 철학입니다.

Spring이 객체 생성과 의존성 연결을 처리하도록 함으로써, 보일러플레이트 코드를 줄이고 비즈니스 로직에 더 집중할 수 있습니다. 작은 REST API를 만들든 대규모 엔터프라이즈 시스템을 구축하든, 의존성 주입을 마스터하면 Java 프로그래밍의 품질이 즉시 향상됩니다.

Back to Blog

관련 글

더 보기 »