의존성 주입 ( Dependency Injection)

김르르
7 min readFeb 6, 2024

소프트웨어 엔지니어링에서 객체가 자신의 의존성, 즉 다른 객체와의 관계나 협력을 필요로 할 때, 이를 외부에서 제공(주입)하는 디자인 패턴.

제어의 역전(Inversion of Control, IoC)의 한 형태로, 객체의 생성과 의존성 관리를 애플리케이션 코드가 아닌 프레임워크에 위임.

DI 의 개념

의존성(Dependency): 한 클래스가 다른 클래스의 인스턴스를 사용하는 경우, 전자는 후자에 대한 의존성을 가짐.

주입(Injection): 의존성을 가지는 객체에 필요한 객체를 외부(프레임워크나 컨테이너)에서 제공하는 과정.

DI의 목적

  • 결합도 감소: 객체 간의 결합도를 줄여, 변경에 더 유연하게 대응할 수 있게 함.

의존성 주입 전

public class PaymentService {
private CreditCardProcessor processor = new CreditCardProcessor();
private TransactionLog transactionLog = new TransactionLog();

public void pay(Order order) {
// 결제 처리 로직
}
}

의존성 주입 후

public class PaymentService {
private CreditCardProcessor processor;
private TransactionLog transactionLog;

@Autowired
public PaymentService(CreditCardProcessor processor, TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}

public void pay(Order order) {
// 결제 처리 로직
}
}

의존성 주입을 사용함으로써, PaymentService는 더 이상 구체적인 CreditCardProcessorTransactionLog의 구현에 의존하지 않는다. 이는 결합도를 줄이고, 유연성을 증가시킴.

  • 코드 재사용성 향상: 재사용 가능한 모듈 또는 클래스를 쉽게 교체하거나 재사용할 수 있음
@Autowired
public PaymentService(CreditCardProcessor processor, TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}
  • 테스트 용이성: 실제 구현 대신 모의 객체(Mock)나 스텁(Stub)을 쉽게 주입할 수 있다.
@Test
public void paymentTest() {
CreditCardProcessor mockProcessor = mock(CreditCardProcessor.class);
TransactionLog stubLog = new StubTransactionLog();
PaymentService service = new PaymentService(mockProcessor, stubLog);

service.pay(new Order());

verify(mockProcessor).charge(any(Order.class));
// 추가 검증 로직
}
  • 응집력 증가: 객체가 자신의 핵심 기능에만 집중할 수 있게 하여, 코드의 응집력을 높임
public class PaymentService {
private CreditCardProcessor processor;
private TransactionLog transactionLog;

@Autowired
public PaymentService(CreditCardProcessor processor, TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}

DI의 방법

- 생성자 주입(Constructor Injection)

생성자 주입은 의존성을 객체 생성 시 생성자를 통해 주입하는 방식. 이 방법은 필수 의존성에 가장 적합하며, 의존성이 불변임을 보장함.

@Component
public class PaymentService {
private final CreditCardProcessor processor;
private final TransactionLog transactionLog;

@Autowired
public PaymentService(CreditCardProcessor processor, TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}

// 비즈니스 로직
}

- 세터 주입(Setter Injection)

세터 주입은 객체 생성 후 세터 메소드를 통해 의존성을 주입하는 방식. 선택적 의존성이나 런타임에 변경될 수 있는 의존성에 적합하다.

@Component
public class PaymentService {
private CreditCardProcessor processor;
private TransactionLog transactionLog;

@Autowired
public void setProcessor(CreditCardProcessor processor) {
this.processor = processor;
}

@Autowired
public void setTransactionLog(TransactionLog transactionLog) {
this.transactionLog = transactionLog;
}

// 비즈니스 로직
}

- 필드 주입(Field Injection)

필드 주입은 스프링 프레임워크가 리플렉션을 사용하여 객체의 필드에 직접 의존성을 주입하는 방식입니다. 코드는 간결해지지만, 의존성이 외부에서 명시적으로 설정되지 않기 때문에 테스트가 어렵고, 코드의 명확성이 떨어질 수 있다.

@Component
public class PaymentService {
@Autowired
private CreditCardProcessor processor;

@Autowired
private TransactionLog transactionLog;

// 비즈니스 로직
}

각 방법의 장단점 요약

  • 생성자 주입

장점: 의존성 불변성 보장, 필수 의존성 명시, 순환 의존성 감지 용이

단점: 많은 의존성을 가질 경우 생성자가 복잡해질 수 있음

  • 세터 주입

장점: 선택적 의존성 주입 용이, 런타임에 의존성 변경 가능

단점: 의존성 변경 가능성으로 인한 불안정성, 객체 완전성 보장 어려움

  • 필드 주입

장점: 코드 간결성

단점: 테스트 어려움, 명시적인 의존성 설정 부재, 순환 의존성 문제 발생 가능성

스프링 프레임워크에서는 가능한 생성자 주입을 사용하는 것을 권장한다. 이는 객체의 불변성을 유지하고, 테스트 용이성을 높이며, 의존성을 명확하게 표현할 수 있기 때문.

--

--