HJW's IT Blog

Dependency Injection 은 무엇이며 왜 사용해야 하는가? 본문

개발 개념

Dependency Injection 은 무엇이며 왜 사용해야 하는가?

kiki1875 2024. 10. 19. 16:34

Dependency Injection(DI) 이란?

“Dependency injection is a programming technique that makes a class independent of its dependencies.”

 

DI 란 객체간의 의존성을 외부에서 주입해주는 설계 패턴이다. 이를 통해 객체는 의존하는 객체들을 직접 생성하는 것이 아닌 외부에서 제공된 객체를 사용하게된다. 이를 통해 개발자는 SOLID 원칙의 의존성 역전 원칙단일 책임 원칙을 달성할 수 있다.

잠깐 의존성 역전 원칙과 단일 책임 원칙을 짚어보고 넘어가겠다.

 

 


 

의존성 역전 원칙 (Dependency Inversion Principle)

DIP 란 무엇일까?

  • DIP 란 상위 모듈이 하위 모듈에 의존하지 않아야 하며, 둘 다 추상화된 인터페이스에 의존해야 한다는 원칙이다. 이를 통해, 결홥도를 낮추며 유연한 설계를 할 수 있다.
  • 여기서 상위 모듈이란 복잡한 로직등을 포함하고 비즈니스 요구를 해결하는 코드이며, 하위 모듈은 DB 접근, 파일 입출력, 네트워크 요청등을 담당하는 모듈이다.
// 추상화된 인터페이스
interface MessageSender {
	void send(String message);
}

// 하위 모듈 1 : email sender
class EmailSender implements MessageSender{
	@Override
	public void send(String message){
		System.out.println("Sending email.." + message);
	}
}

// 하위 모듈 2 : Sms Sender
class SmsSender implements MessageSender{
	@Override
	public void send(String message){
		System.out.println("Sending sms..." + message);
	}
}

// 상위 모듈 : 인터페이스에 의존하여 다양한 전송 지원
class UserNotification {
	private MessageSender messageSender;
	
	// DIP 적용 : 상위 모듈이 구체 클래스가 아닌 인터페이스에 의존
	public UserNotification(MessageSender messageSender){
		this.messageSender = messageSender;
	}
	
	public void notifyUser(String message){
		messageSender.send(message);
	}
}

public class Main{
	public static void main(String[] args){
		MessageSender emailSender = new EmailSender();
		UserNotification emailNotification = new UserNotification(emailSender);
		emailNotification.send("email");
		
		MessageSender smsSender = new SmsSender();
		UserNotification smsNotification = new UserNotification(smsSender);
		smsNotification.send("SMS");
	}
}

 


 

 

단일 책임 원칙 (Single Responsibility Principle, SRP)

SRP 란 하나의 클래스는 하나의 책임만 가져야 한다는 원칙이다. 즉, 하나의 클래스가 여러가지 기능을 담당하는 것이 아니라 오직 한가지 기능만 수행해야 한다는 의미이다.

class Order {
	private int orderId;
	private double orderAmount;
	
	public Order(int orderId, double orderAmount){
		this.orderId = orderId;
		this.orderAmount = orderAmount;
	}
	
	public double calculateTotal(){
		return orderAmount;
	}
	
	public void printOrder(){
		System.out.println(orderId);
	}
}

위 예시를 보면 Order class 는 주문관련 로직 + 출력하는 로직을 담당하기에 단일 책임 원칙을 지키지 못한것이다.

다음과 같이 수정할 경우 SRP 를 만족하게 된다

class Order {
	private int orderId;
	private double orderAmount;
	
	public Order(int orderId, double orderAmount){
		this.orderId = orderId;
		this.orderAmount = orderAmount;
	}
	
	public double calculateTotal(){
		return orderAmount;
	}
	
	public getOrderId(){
		return orderId;
	}
}

class OrderPrinter{
	public void printOrder(Order order){
		Systm.out.println(order.getOrderId());
	}
}

public class Main{
	public static void Main(String[] args){
		Order order = new Order(1, 100);
		OrderPrinter orderPrinter = new OrderPrinter();
		orderPrinter.printOrder(order);
	}
}

 


 

 

왜 Dependency Injection 이 필요한가?

하나의 객체가 다른 객체를 직접 생성하게 되면 두 객체는 강하게 결합된다. 이는 코드의 재사용성을 떨어뜨릴뿐만 아니라, 테스트 또한 하기 어렵게 만든다.

예를 들어, 클래스 A 가 클래스 B를 직접 생성한다면, B가 바뀔 때 마다, A 또한 바꾸어 주어야 한다. 뿐만 아니라, A 를 테스트 하기 위해 항상 B도 생성해야 하기 때문에 단위 테스트가 복잡해 질 수 있다.

Dependency Injection 을 사용하게 되면, A는 B에 대한 구체적인 정보를 알 필요가 없게 되며, 외부에서 B를 제공받기 때문에 B가 바뀐다 한들, A는 영향을 받지 않게 된다. 이로써, 의존성 주입을 통한 느슨한 결합을 달성할 수 있다.

주요 개념

  1. 의존성 : 한 객체가 다른 객체의 기능을 사용할 경우, 그 객체에 의존한다고 말한다. 예를 들어 서비스 클래스가 데이터베이스 레포지토리에 의존한다면, 이 레포지토리 객체가 서비스의 의존성이다.
  2. 주입 : 의존성을 직접 생성하지 않고 외부에서 전달받는 과정이다.
  3. 의존성 역전 원칙 : 객체는 구체적인 클래스에 의존하지 않아야 하며, 추상화된 인터페이스나 상위 클래스에 의존해야 한다.
  4. Inversion of Control : 객체의 생명 주기와 의존성 관리를 외부에서 제어한다는 원칙이다. Spring 의 DI 컨테이너와 같은 메커니즘을 통해 의존성을 주입하고 객체 생성의 책임을 위임한다

 

 


 

 

Dependency Injection 의 방식

Dependency Injection 은 크게 3가지 방식이 있다.

 

생성자 주입

의존성을 객체의 생성자를 통해 주입하는 방법으로, 의존성이 필수적일때 사용한다. 위에서 언급한 UserNotification 이 Setter Injection 의 예시이다.

class UserNotification {
	private MessageSender messageSender;
	
	//의존성 주입
	public UserNotification(MessageSender messageSender){
		this.messageSender = messageSender;
	}
	
	public void notifyUser(String message){
		messageSender.send(message);
	}
}

 

Setter 주입

Setter 를 통해 의존성을 주입하는 방법이다. 위의 생성자 주입의 경우, 객체가 생성됨과 동시에 주입이 되지만, Setter 주입의 경우, 개발자가 직접 해당 메서드를 통해 주입해주어야 한다.

class UserNotification {
    private MessageSender messageSender;

    // Setter 주입
    public void setMessageSender(MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    public void notifyUser(String message) {
        messageSender.send(message);
    }
}

public class Main {
    public static void main(String[] args) {

        MessageSender emailSender = new EmailSender();
        
        // 주입 대상 객체 생성 후, 의존성 주입
        UserNotification notification = new UserNotification();
        notification.setMessageSender(emailSender);
        notification.notifyUser("email");
    }
}

 

필드 주입

필드 주입은 주로 프레임워크를 사용할 때 사용되며, Spring 의 경우 @Autowired 어노테이션을 통해 DI 컨테이너가 직접 객체 필드에 의존성을 주입하는 방식이다.

interface MessageSender{
	void send(String message);
}

@Component
class EmailSender implements MessageSender{
	@Override
	public void send(String message){
		System.out.println("Sending email..." + message);
	}
}

@Component
class UserNotification{
	@Autowired
	private MessageSender messageSender;
	
	public void notifyUser(String message){
		messageSender.send(message);
	}
}

@SpringBootApplication
public class MainApplication{
	public static void main(String[] args){
		ApplicationContext context = SpringApplication.run(MainApplication.class, args);
		UserNotification notification = context.getBean(UserNotification.class);
		notification.notifyUser("Email");
	}
}

 


 

 

Dependency Injection 의 장점

  1. 테스트 독립성 : Mock 객체를 통해 DB나 네트워크에 의존하지 않고 독립적인 테스트를 수행할 수 있다
  2. 유연한 테스트 환경 : 다양한 시나리오를 쉽게 설정하여 다양한 상황에 대한 시뮬레이션이 가능하다
  3. 테스트 유지보수성 : 객체간 결합도가 낮기에 코드 변경시 테스트 코드가 깨질 확률이 낮다

다음은 테스트 상황의 예시이다.

public class UserNotificationTest {

    private MessageSender mockMessageSender; // Mock 객체
    private UserNotification userNotification;

    @Before
    public void setUp() {
        // MessageSender의 Mock 객체 생성
        mockMessageSender = mock(MessageSender.class);

        // UserNotification에 Mock 객체 주입
        userNotification = new UserNotification(mockMessageSender);
    }

    @Test
    public void testNotifyUser() {
        // 테스트할 메시지
        String message = "Test message";

        // 유저에게 알림 전송
        userNotification.notifyUser(message);

        // Mock 객체의 send 메소드가 해당 메시지로 호출되었는지 검증
        verify(mockMessageSender).send(message);
    }

    @Test
    public void testNotifyUserWithDifferentMessage() {
        // 다른 메시지로 테스트
        String anotherMessage = "Another test message";

        // 유저에게 알림 전송
        userNotification.notifyUser(anotherMessage);

        // Mock 객체가 호출된 횟수와 메시지 검증
        verify(mockMessageSender, times(1)).send(anotherMessage);
    }
}