본문 바로가기
Language/Java

[JAVA] 객체 지향의 정수, 의존성 주입(Dependency Injection) 완벽 이해하기

by Papa Martino V 2026. 1. 17.
728x90

Dependency Injection
Dependency Injection

 

자바 개발자로서 '결합도(Coupling)'와 '응집도(Cohesion)'라는 단어는 귀에 못이 박히도록 들으셨을 겁니다. 좋은 소프트웨어는 낮은 결합도와 높은 응집도를 가져야 한다고 하죠. 하지만 막상 실무 코드를 짜다 보면 객체와 객체가 서로 얽히고설켜, 코드 한 줄 고치기가 무서운 상황이 발생하곤 합니다. 이런 문제를 해결하기 위해 등장한 개념이 바로 의존성 주입(Dependency Injection, DI)입니다. 오늘은 DI가 왜 필요한지, 그리고 단순한 문법적 지식을 넘어 소프트웨어 설계적 관점에서 어떤 가치를 제공하는지 심도 있게 살펴보겠습니다.


1. 의존성(Dependency)이란 무엇인가?

DI를 이해하기 전에 먼저 '의존성'의 본질을 이해해야 합니다. 프로그래밍에서 의존성이란 한 클래스가 기능을 수행하기 위해 다른 클래스를 참조하거나 호출하는 상태를 의미합니다. 예를 들어, CoffeeMaker 클래스가 ElectricHeater 클래스를 직접 생성해서 사용하고 있다면, CoffeeMakerElectricHeater에 의존하고 있는 것입니다. 만약 나중에 GasHeater로 교체하고 싶다면 CoffeeMaker의 소스 코드를 직접 수정해야 합니다. 이것이 바로 '강한 결합(Tight Coupling)'의 전형적인 사례입니다.


2. 의존성 주입(DI)의 핵심 개념

의존성 주입은 객체가 직접 자신이 사용할 객체를 생성(new)하지 않고, 외부(프레임워크나 컨테이너)에서 생성된 객체를 주입받아 사용하는 방식을 말합니다. 이는 '제어의 역전(Inversion of Control, IoC)'이라는 더 큰 개념의 구체적인 구현체이기도 합니다.

왜 '주입'인가?

레스토랑을 예로 들어보겠습니다. 셰프(Chef)가 요리를 하기 위해 칼(Knife)이 필요합니다.

  • 기존 방식: 셰프가 요리 도중에 직접 대장간에 가서 칼을 만들어옵니다.
  • DI 방식: 셰프는 가만히 있고, 주방 보조가 셰프가 사용할 칼을 쟁반에 담아 전달(주입)해 줍니다.

셰프는 칼이 어떻게 만들어졌는지 알 필요가 없습니다. 그저 전달받은 칼로 요리만 하면 됩니다. 이것이 DI의 본질입니다.


3. 의존성 주입의 3가지 유형

자바에서 DI를 구현하는 방법은 크게 세 가지가 있습니다. 현대적인 Spring 프레임워크 환경에서는 생성자 주입을 가장 권장합니다.

유형 특징 장점 단점
생성자 주입 (Constructor Injection) 객체 생성 시점에 생성자를 통해 주입 불변성 확보, 필수 의존성 누락 방지, 테스트 용이 파라미터가 많아질 경우 가독성 저하
Setter 주입 (Setter Injection) 설정자 메서드를 통해 주입 선택적인 의존성 주입 시 유리함 객체 생성 후 의존성 변경 위험(불변성 위배)
인터페이스 주입 (Interface Injection) 주입받는 인터페이스를 구현하여 주입 구조가 명확함 클래스 구조가 복잡해져 현대 자바에선 드묾

4. DI를 사용하면 얻게 되는 실질적인 이점

DI는 단순히 코드가 멋있어 보이려고 사용하는 것이 아닙니다. 비즈니스 로직의 유연성을 극대화하기 위한 도구입니다.

  • 코드의 재사용성 향상: 의존성 객체가 독립적이므로 다른 프로젝트나 클래스에서도 쉽게 가져다 쓸 수 있습니다.
  • 테스트 용이성(Testability): 실제 DB 연결 대신 가짜 객체(Mock Object)를 주입하여 단위 테스트를 수행하기 매우 쉬워집니다.
  • 유지보수 비용 감소: 구현체가 바뀌더라도(예: Oracle DB -> MySQL DB) 주입 설정만 바꾸면 되며, 비즈니스 로직을 담은 코드는 수정할 필요가 없습니다.
  • 확장성: 새로운 기능을 추가할 때 인터페이스를 기반으로 구현체만 갈아 끼우면 되므로 개방-폐쇄 원칙(OCP)을 준수하게 됩니다.

5. 실전 코드 예시: 생성자 주입

가장 권장되는 방식인 생성자 주입의 자바 코드를 살펴보겠습니다.


// 1. 인터페이스 정의
public interface MessageService {
    void sendMessage(String msg);
}

// 2. 구현체 제작
public class EmailServiceImpl implements MessageService {
    @Override
    public void sendMessage(String msg) {
        System.out.println("Email 전송: " + msg);
    }
}

// 3. 의존성을 주입받는 클래스
public class NotificationController {
    private final MessageService messageService;

    // 생성자를 통한 주입 (Constructor Injection)
    public NotificationController(MessageService messageService) {
        this.messageService = messageService;
    }

    public void send(String message) {
        messageService.sendMessage(message);
    }
}

위 코드에서 NotificationControllerEmailServiceImpl의 존재를 모릅니다. 단지 MessageService 인터페이스만 알고 있을 뿐입니다. 나중에 SMS 서비스로 바꾸고 싶다면 코드를 건드리지 않고 외부에서 SmsServiceImpl 객체만 넣어주면 됩니다.


마치며: DI는 선택이 아닌 필수입니다

객체 지향 프로그래밍의 핵심은 '객체 간의 협력'입니다. 의존성 주입은 이 협력을 가장 유연하고 견고하게 만들어주는 마법 같은 도구입니다. 처음에는 설정이 번거롭고 개념이 생소할 수 있지만, 대규모 프로젝트나 유지보수가 중요한 시스템에서는 DI 없는 설계를 상상하기 어렵습니다. 지금 작성하고 있는 코드에 new 키워드가 너무 남발되고 있지는 않나요? 혹시 클래스가 너무 많은 책임을 지고 다른 객체의 생성까지 관여하고 있지는 않은지 되돌아보시길 바랍니다.


참고 문헌 및 출처

  • Martin Fowler, "Inversion of Control Containers and the Dependency Injection pattern"
  • Robert C. Martin, "Clean Code: A Handbook of Agile Software Craftsmanship"
  • Oracle Java Documentation: Object-Oriented Programming Concepts
  • Spring Framework Reference Documentation: Dependency Injection
728x90