DI(Dependency Injection, 의존성 주입) / IoC(Inversion of Control, 제어의 역전) 이 무엇인가?

최경현·2024년 9월 8일

스프링을 사용하다보면 DI와 IoC라는 단어를 듣게 되는데
이게 무엇일까?

IoC(Inversion of Control)란?

IoC란 영어 그대로 제어의 역전이라 부른다.
프로그램의 제어 흐름을 개발자가 아니라 프레임워크가 담당하는 것을 의미한다.
객체의 생성과 호출을 프레임워크가 관리하고, 개발자는 그 객체들이 무엇을 해야 하는지에만 집중하면 된다.

간단하게 예시들 들어보자.
결제하기 위한 인퍼테이스가 아래에 처럼 구현되어 있다고 해보자.

// 결제 서비스 인터페이스
public interface PaymentService {
    void processPayment();
}

// CreditCard 결제 서비스
public class CreditCardPaymentService implements PaymentService {
    @Override
    public void processPayment() {
        System.out.println("결제 방법은 CreditCard로.");
    }
}

// PayPal 결제 서비스
public class PayPalPaymentService implements PaymentService {
    @Override
    public void processPayment() {
        System.out.println("결제 방법은 paypal로.");
    }
}

IoC 적용 전

public class OrderService {
    private PaymentService paymentService;

    // 특정 결제 서비스 구현체를 직접 선택하고 생성
    public OrderService() {
        // IoC 적용 전: 직접 결제 서비스 선택
        this.paymentService = new CreditCardPaymentService();  // CreditCard 서비스 사용
        // 만약 PayPal을 사용하고 싶으면 직접 아래처럼 수정해야 함
        this.paymentService = new PayPalPaymentService();
    }

    public void placeOrder() {
        System.out.println("Order placed.");
        paymentService.processPayment();
    }
}

즉, IoC를 적용하지 않으면 방식이 바뀔 때 마다 직접 해당 구현체를 선택하고 코드를 수정해야한다.

IoC 적용 후

public class OrderService {
    private PaymentService paymentService;

    // IoC 적용: 외부에서 PaymentService 객체를 주입받음
    public OrderService(PaymentService paymentService) { // 결제 방식을 여기에 주입하면 됨.
        this.paymentService = paymentService;  // 외부에서 주입된 객체를 사용
    }

    public void placeOrder() {
        System.out.println("Order placed.");
        paymentService.processPayment();  // 주입된 객체의 메서드를 호출
    }
}

즉, IoC를 적용하면 코드를 수정할 필요 없이 외부에서 주입된 객체를 이용해서 여러 방식을 쉽게 변경하거나 추가가 가능하다.

IoC는 객체간의 결합도를 낮추고, 코드의 유연성과 유지보수성을 엄청나게 높여준다.

DI(Dependency Injection)이란?

DI란 IoC의 구체적인 구현 방식 중 하나이다. 간단히 말하자면,
필요한 객체(의존성)를 외부에서 주입받는 방식이다.
DI는 프로그램의 객체 간의 결합도를 줄이고, 유연성을 높이는 설계 패턴이다.

DI의 종류에는 세가지가 있다.

  • 생성자 주입
  • 세터 주입
  • 필드 주입

1. 생성자 주입

  • 객체 생성 시점에 생성자를 통해 의존성을 주입받음.
  • 주입받은 의존성을 불변으로 유지할 수 있다. (final)
  • 가장 권장되는 방법이다.
public class OrderService {
    private final PaymentService paymentService;

    // 생성자 주입
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void placeOrder() {
        // 주문 로직 처리
        paymentService.processPayment();
    }
}

2. 세터 주입

  • 객체 생성 후, 세터 메서드를 통해 의존성을 주입받음.
  • 의존성을 변경할 수 있는 유연성이 있음.
  • 대신 주입을 강제하지 못해 의존성이 누락될 위험이 있음. (의존성 주입이 필수인데, 호출 누락시 NullPointerException 발생할 수 있음.)
public class OrderService {
    private PaymentService paymentService;

    // 세터 주입
    public void setPaymentService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void placeOrder() {
        paymentService.processPayment();
    }
}

3. 필드 주입

  • 필드에 직접 주입하여 사용
  • 코드가 간결함.
  • 테스트가 어렵고 의존성 주입이 명시적이지 않아 권장되지 않음. (Autowired 어노테이션을 통해 자동으로 주입됨. 하지만 어디서 어떻게 주입되는지 명확하지 않음.)
public class OrderService {
    @Autowired // 스프링에서 사용
    private PaymentService paymentService;

    public void placeOrder() {
        paymentService.processPayment();
    }
}

요약하자면,
IoC와 DI는 객체 간의 의존성을 낮추고, 유연하고 유지보수하기 쉬운 코드를 작성할 수 있도록 도와준다.
대신 초기 설정이나 복잡성 증가 같은 단점이 있을 수 있으며, 객체 간의 관계를 추적하기 어려워 질 수도 있다.

즉, 복잡한 시스템이나 대규모 애플리케이션에서 매우 유용하며, 시스템의 유지보수성과 확장성을 크게 향상시킬 수 있는 패턴이다.

출처
https://mozzi-devlog.tistory.com/18

profile
ㅇㅇ

0개의 댓글