의존성 주입(Dependency Injection)

younk·2023년 10월 10일
0

스프링부트

목록 보기
5/10

의존성 주입(Dependency Injection)이란

클래스간 의존성을 클래스 외부에서 주입하는 것을 의미한다.
객체지향 프로그래밍에서 클래스간 의존성이 있다는 것은, 클래스간의 의존관계가 존재함을 의미한다. 이는 한 클래스가 바뀔때 다른 클래스가 영향을 받는다는 의미이기도 하다.
DI는 두 클래스간의 관계를 정의하기 위해 중간에 인터페이스를 두고, 클래스 레벨에서는 의존관계가 고정되지 않도록 하고, 런타임 시에 관계를 동적으로 주입해준다. 이는 코드의 유연성을 늘리고 결합도를 낮출 수 있도록 해준다.

의존성 주입이 필요한 이유

public class Computer {
	private Monitor monitor;
    
    public Computer() {
    	this.monitor = new Monitor();
	}
}

위 코드의 문제점은 크게 두가지가 있다.

  • 두 클래스가 강하게 결집되어 있다.
    Monitor클래스가 Monitor15로 업그레이드 된다면, Computer 클래스는 2줄의 코드를 수정해야한다. 현 예시에선 2줄뿐이지만, 실제로 사용되는 클래스가 Monitor 뿐아니라 Mouse, KeyBoard, Speaker 등등 여러개라면, 수정해야할 코드는 10줄 100줄이 될 수도 있다.

  • 객체들 간의 관계가 아니라 클래스 간의 관계가 맺어져 있다.
    올바른 객체지향적 설계는 객체들 간의 관계를 지향한다. 객체들 간 관계가 맺어져있다면, 다른 객체의 구체 클래스를 전혀 알지 못하더라도 인터페이스 타입으로 사용할 수 있다.

결국 클래스간 결합도가 높아지고, 코드가 유연해질수 없기에, 스프링에서는 DI를 적용하여 문제를 해결할 수 있다.

다형성을 이용한 의존성 주입

public interface Element {
}

public class Monitor implements Element {
}

public class Keyboard implements Element {
}
...

위 코드에선 Element라는 인터페이스를 두어 여러가지 구성품을 하나로 표현할수 있게 되었다.

public class Computer {
	private Element element;
    
    public Computer(Element element) {
    	this.element = element;
	}
}

Computer 클래스에서 element를 의존하면, Computer 생성자의 매개변수로 어떤 구성품이 오든 코드를 변경하지 않을 수 있다. 또한 Computer에서 Element 객체를 주입하기 위해서는 애플리케이션 실행 시점에 필요한 객체를 생성해야하며, 의존성이 있는 두 객체를 연결하기 위해 한 객체를 다른 객체로 주입시켜야 한다. 이러한 역할을 스프링 프레임워크가 해준다.

public class BeanFactory {

    public void computer() {
        // Bean의 생성
        Element monitor = new Monitor();
    
        // 의존성 주입
        Computer computer = new Computer(monitor);
    }
    
}

위 코드와 같은 DI 컨테이너를 스프링이 지원하기 때문에, DI 컨테이너라고도 불린다. 또한 이런 개념은 제어의 역전(Inversion of Control, IoC)이라고 불리기도 한다. 어떤 객체를 사용할 지에 대한 책임은 프레임워크에게 넘어가고, 개발자는 수동적으로 주입받는 객체를 사용하기 때문이다.

DI 방식 세가지

스프링의 DI 방식은 세가지가 있다.
Spring 3버전까지는 setter 주입을 권장했으나, 4.3부터는 생성자 주입을 권장하고 있다.

1) 생성자 주입

생성자에 의존성 주입을 받고자하는 필드를 나열하는 방법이다.
@Autowired 어노테이션을 통해 생성자를 주입할 수 있다. 생성자가 하나만 있을 경우, @Autowired를 생략해도 주입이 가능하다.
final 키워드를 사용할 수 있기 때문에, 생성자로 인스턴스가 생성될때 1번만 할당되고, 객체의 불변성을 보장할 수 있다. 또한 초기에 할당되기때문에 NPE(Null Pointer Exception)이 발생하지 않는다.

@Component
public class UserService {
    private final UserRepository userRepository;
    private final MemberService memberService;

    @Autowired //생략가능
    public UserService(UserRepository userRepository, MemberService memberService) {
        this.userRepository = userRepository;
        this.memberService = memberService;
    }
}

2) 필드 주입

필드에 @Autowired를 붙여서 바로 주입하는 방법이다.
final 선언이 불가능하다.
의존 관계가 잘 보이지 않아 추상적이고, 이로 인해 의존성 관계가 복잡해질 수 있다.
생성자주입보다 코드가 간결하지만, 외부에서 변경이 불가능하여 테스트하기 어렵다.
DI 프레임 워크가 없다면 아무것도 할 수 없으며, SRP(단일책임원칙)에 위배되는 안티패턴이다.

@Component
public class UserService {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private MemberService memberService;

}

3) setter 주입

setter 메소드에 @Autowired 어노테이션을 선언하여 주입받는 방법이다.
final 선언 불가능하다.
의존성이 선택적으로 필요한 경우 사용할 수 있기 때문에, 생성자 주입방법과 setter 주입 방법을 적절하게 분배하여 사용할 수 있다.

public class UserService {

    private UserRepository userRepository;
    private MemberService memberService;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Autowired
    public void setMemberService(MemberService memberService) {
        this.memberService = memberService;
    }
}

참고 사이트

[Spring] 의존성 주입(Dependency Injection, DI)이란? 및 Spring이 의존성 주입을 지원하는 이유
[Spring] 다양한 의존성 주입 방법과 생성자 주입을 사용해야 하는 이유 - (2/2)
[Spring] @Autowired란 무엇인가?

0개의 댓글