스프링의 핵심 개념 중 하나인 DI(Dependency Injection 의존관계 주입/의존성 주입)
에 대해서 알아보겠습니다.
우선 네이버 사전 기준으로 의존성
에 대한 설명은 다음과 같습니다.
프로그래밍에서 의존성
은 두 클래스 간의 연결 관계를 말하게 됩니다.
만약 A클래스에서 B클래스의 메소드 등을 호출해서 사용한다면 A클래스는 B클래스 객체를 생성(파라미터, 리턴, 지역변수 등)해서 참조하게 됩니다.
이때 A클래스는 B클래스에 의존한다. 라고 표현하게 됩니다. B클래스의 메소드가 사라지면 A클래스는 제대로 작동하지 않을 것이며 B클래스의 메소드가 변경된다면 A클래스의 동작 또한 변경됩니다.
예를 들어 운전자는 차에 탑승해서 시동을 걸고 악셀을 밟아야 차가 앞으로 나갑니다.
이 상황을 코드로 표현해보면 Driver 클래스는 Vehicle 클래스의 accelerator() 메소드를 호출하게 됩니다. 이때 Driver 클래스는 Vehicle 클래스에 의존한다라고 표현할 수 있습니다.
class Driver {
private Vehicle vehicle = new Vehicle();
public void pressAccelerator() {
vehicle.accelerator();
}
}
추가로 Vehicle 클래스의 accelerator() 메소드 내용이 변경된다면 Driver.pressAccelerator()의 동작도 변하겠죠? 또는 메소드가 삭제된다면 Driver 클래스가 제대로 동작하지 않게 됩니다.
자 그러면 의존관계에 대해 알아봤으니 스프링에서의 의존관계 주입
이라는 개념은 무엇을 이야기하는지 알아볼 차례입니다.
스프링 의존관계 주입
은 프로그래머가 코드로 직접 명시하는 것이 아니라 외부로부터 의존관계를 주입받는 것을 의미합니다.
//의존관계 주입 전
class Driver {
private Vehicle vehicle = new Vehicle();
public void pressAccelerator() {
vehicle.accelerator();
}
}
위 코드 처럼 객체 내부에서 직접 의존관계를 받는 것이 아닌 아래 처럼 외부를 통해 객체가 선언되고 주입하는 것을 의미합니다.
//외부에서 객체 선언 후 주입
class Main{
public static void main(String[] args) {
Vehicle vehicle = new Vehicle(); //외부 클래스에서 객체 생성후
Driver driver = new Driver(vehicle); //Driver 객체 생성 시 Vehicle 객체를 생성자를 통해 전달
driver.pressAccelerator();
}
}
class Driver {
private final Vehicle vehicle;
public Driver(Vehicle vehicle) { //Driver객체는 생성자를 통해서 외부로부터 Vehicle객체를 주입받는다.
this.vehicle = vehicle;
}
public void pressAccelerator() {
vehicle.accelerator(); //주입받은 객체의 메소드 호출
}
}
외부 주입을 통해 주입대상이 B 객체에서 다른 객체로 수정될 경우 주입을 하는 외부 로직만 수정할 뿐이지 A 클래스 내부의 구현은 전혀 수정이 필요하지 않게 됩니다.
이는 자연스럽게 SOLID 원칙 중 OCP(확장에는 열려있고, 변경에는 닫혀야 한다.)를 지킬 수 있게 됩니다.
위 예시에서 주입받는 객체가 Vehicle을 상속한 Sedan 객체가 오던 Truck 객체가 오던 Driver 객체 구현에는 영향을 주지 않게 됩니다.
class Main{
public static void main(String[] args) {
Vehicle vehicle = new Sedan(); //이 부분만 수정하면 끝.
Driver driver = new Driver(vehicle);
driver.pressAccelerator();
}
}
class Sedan extends Vehicle {}
스프링에서 의존관계 주입은 스프링 빈들이 주입되게 됩니다. 이는 곧 스프링 컨테이너가 관리하는 객체들이 주입이 된다는 것을 의미합니다.
스프링 빈과 스프링 컨테이너에 대한 것은 이 포스트를 참조해주세요.
스프링 컨테이너가 관리하는 빈
- 수동 등록에서
@Configuration
에 등록된@Bean
들- 자동 등록(
@ComponentScan
)에서@ComponentScan 패키지의 클래스들
,@Component, @Service, @Repository, @Controller
과 같은@Component
어노테이션을 가진 클래스들진 클래스들
스프링 의존관계 주입 방법에는 생성자 주입, Setter 주입, 필드 주입, 메소드 주입
네 가지 방식이 있습니다.
스프링에서 현재 권장하고 있는 방법입니다. 이 방법을 주로 쓰되 나머지 방법도 알아두는 것이 좋습니다.
생성자 주입
은 생성자를 통해서 의존관계를 주입하는 방법입니다. 위에서 Driver와 Vehicle 예시를 들 때 사용했던 방법이 바로 생성자 주입
입니다.
```java
class Main{
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
Driver driver = new Driver(vehicle);
driver.pressAccelerator();
}
}
@Component
class Driver {
private final Vehicle vehicle;
@Autowired
public Driver(Vehicle vehicle) { //생성자 주입
this.vehicle = vehicle;
}
public void pressAccelerator() {
vehicle.accelerator();
}
}
이 방식은 생성자가 호출되는 시점에 주입이 되기 때문에 객체 생성 시점에 단 한 번의 주입을 보장하게 됩니다.
그렇기 때문에 주입 후 불변하거나 필수로 사용되는 의존성을 주입할 때 이 방식을 사용하게 됩니다.
아까랑 다르게 @Autowired
라는 어노테이션이 하나 붙은 것을 볼 수 있습니다.
@Autowired
는 의존관계 주입에 필요한 객체 타입을 찾아서 자동을 빈을 주입해주는 일을 합니다. 그렇기에 당연히 스프링 컨테이너에 등록된 스프링 빈에 대해서만 사용할 수 있는 어노테이션입니다.
다음 세 가지 특징을 가지고 있습니다.
생성자 주입, setter 주입, 필드 주입
세 가지 방식에 대해서만 사용할 수 있다.생성자 주입
에서 생성자가 단 하나라면 @Autowired
를 생략할 수 있다. (Spring 4.3.X 이상)final
키워드로 선언할 수 있어서 에러를 방지할 수 있다.즉, 위 상황에서 Vehicle 객체가 스프링 빈으로 등록되었다면 Driver의 생성자에서
@Autowried
를 통해 자동으로 빈을 찾아서 주입할 수 있게 됩니다.
추가적으로
@Autowired
는 스프링에서 제공하는 어노테이션이므로 테스트 또한@SpringBootTest
등을 이용해 스프링 컨테이너를 테스트에 통합시켜서 사용할 경우에만 동작합니다.
@Autowired
는 주입할 빈을 찾을 때 타입
을 기준으로 빈을 조회합니다. 이 상황에서 하위 타입을 두 개 가진 부모 타입으로 조회하는 경우 동일 타입이 두 개가 존재하기 때문에 오류가 발생합니다.
@Component
public class Sedan implements Vehicle {}
@Component
public class Truck implements Vehicle {}
...
@Autowired
private final Vehicle vehicle; // <- 에러 발생!!! 중복 빈
이럴때 해결 방안으로는 필드명 매칭, @Qualifier, @Primary
세 가지 방법으로 해결할 수 있습니다.
필드명 매칭
은 @Autowired
의 동작을 이용한 해결 방식입니다.@Autowired
은 먼저 타입 조회를 한 후 동일 빈이 있다면 필드/파라미터 이름으로 추가 조회를 시도하게 됩니다. 따라서 필드/파라미터 이름을 주입하고자하는 하위 타입 이름으로 변경하면 중복 문제를 해결할 수 있습니다.@Autowired
private final Vehicle vehicle; //기존 주입
@Autowired
private final Vehicle sedan; //필드명 변경
@Qualifier
@Qualifier
는 식별할 수 있도록 구분자를 추가하는 어노테이션입니다. 빈 등록시 함께 지정해서 빈의 구분자를 더할 수 있게 됩니다. 주의할 점은 빈 이름을 변경하는 것이 아니다라는 것 입니다.
빈에 구분자를 적은 후 주입 시점에 @Qualifier
를 사용해서 주입할 구분자를 적어주면 해당 구분자를 가진 빈이 주입됩니다.
@Component
@Qualifier("mainVehicle")
public class Sedan implements Vehicle {}
@Component
@Qualifier("subVehicle")
public class Truck implements Vehicle {}
...
@Autowired
public Driver (@Quilfier("mainVehicle") Vehicle vehicle) {
this.vehicle = vehicle; // <- Sedan 빈이 주입된다.
}
@Primary
는 어노테이션을 선언한 빈이 여러 빈 조회 시 우선권을 갖도록 만드는 어노테이션입니다.@Component
@Primary
public class Sedan implements Vehicle {}
@Component
public class Truck implements Vehicle {}
...
@Autowired
public Driver (Vehicle vehicle) {
this.vehicle = vehicle; // <- Sedan 빈이 주입된다.
}
Setter 주입
은 Setter 메소드를 통해서 의존관계를 주입하는 방법입니다.
이 방식은 변할 가능성이 있거나 필수가 아닌 선택적 의존관계를 주입할 때 사용합니다.
@Component
class Driver {
private Vehicle vehicle;
@Autowired
public void setVehicle(Vehicle vehicle) {
this.vehicle = vehicle;
}
}
필드 주입
은 필드에 바로 주입하는 방법입니다.
생성자를 만들어야하는 생성자 주입니다 Setter를 만들어야하는 Setter 주입에 비해 간결해서 좋아보이지만 단점이 있습니다.
두 가지 치명적인 단점 때문에 실제 코드에서는 거의 사용하지 않는다고 합니다. 다만 용도가 아예 없는 것은 아니고 테스트 코드나 스프링 설정 코드에서는 특별하게 사용해도 괜찮다고 합니다.
일반 메소드 주입
은 생성자나 Setter가 아닌 메소드를 이용해서 주입받는 방식입니다. 한 번에 여러 필드에 주입할 수도 있다는 장점은 있지만 여러가지 프로그래밍 원칙에 위배되는 방식이라서 이런 방식이 있다 정도로만 해두고 넘어가시면 됩니다.
위에서도 언급했지만 의존관계 주입을 할 때 생성자 주입
을 권장하고 있습니다.
그 이유는 다음과 같습니다.
위와 같은 이유 때문에 의존관계를 주입할 때는 생성자 주입
방식을 우선적으로 염두에 두고 설계하는 것이 좋습니다.