이전 포스트에서 DI에 대해서 알아봤다. 스프링은 조립기 역할을 한다고 했다. 코드를 보자.
@Configuration
public class AppCtx {
@Bean
public ImageDao imageDao() {
return new ImageDao();
}
@Bean
public ImageLoadingService imageLoadingService() {
return new ImageLoadingService(imageDao());
}
...
}
위 코드를 보면 ImageDao 객체를 빈으로 등록한후 ImageLoadingService에 imageDao()를 통해 의존성을 주입시켜주고 있는 것을 볼 수있다. 이렇게 직접 의존성을 주입시켜주는 방법도 있지만 스프링은 이를 자동으로 주입시켜주는 기능이 있다.
ImageLoadingService 코드를 보면 필드로 ImageDao를 가지고 ImageLoadingService가 생성될때 초기화가 이루어진다. 스프링의 @Autowired를 사용하면 직접 의존성을 주입할 필요없이 스프링이 해당 객체를 찾아 자동으로 의존성을 주입해준다.
public class ImageLoadingService {
@Autowired
private ImageDao imageDao;
...
}
이렇게 의존성이 필요한 필드에 @Autowired만 붙여주면 스프링이 해당 객체가 빈으로 등록되어 있으면 해당 빈을 찾아서 의존성을 주입해준다. 이렇게 필드를 위에 @Autowired를 붙여도 되고 메서드 위에도 @Autowired를 붙일 수 있다.
public class ImageUpdateService {
private ImageDao imageDao;
...
@Autowired
public void setImageDao(ImageDao imageDao) {
this.imageDao = imageDao;
}
}
이렇게 setter 메서드에 @Autowired를 붙여주면 파라미터 타입과 같은 타입의 빈을 찾아서 의존성을 주입해준다. 이렇게 @Autowired를 붙여준 후 위에서 조립기 역할을 하는 @Configuration 설정 클래스에 직접 의존성을 주입하는 부분을 생략해준다.
@Configuration
public class AppCtx {
@Bean
public ImageDao imageDao() {
return new ImageDao();
}
@Bean
public ImageLoadingService imageLoadingService() {
return new ImageLoadingService();
}
}
이렇게 직접 의존성을 주입하는 코드를 제거한 후 테스트를 실행해보면 정상적으로 작동하는 것을 볼 수 있다.
@Autowired를 붙인 필드나 메서드에 해당하는 빈이 없다면 UnsatisfiedDependencyException이 발생한다. 이는 해당 타입의 빈이 존재하지 않아 의존성을 주입할 수 없다는 것을 의미한다. 따라서 @Autowired를 붙인 필드나 메서드의 타입이 빈이 꼭 필요하다.
그렇다면 똑같은 타입의 빈이 여러 개 있을 때 어떻게 스프링은 어떤것을 선택할까? 일단 이런 경우가 발생되면 오류가 발생한다. 에러 메시지는 해당 빈이 한 개가 아니라 이름이 여러개인 빈이 존재한다고 알려준다. 하지만 자동 주입을 하기 위해서는 해당 타입을 가진 빈이 어떤 빈인지 정확하게 한정할 수 있어야 한다.
그래서 이럴 경우 @Qualifier 어노테이션을 이용해서 의존 객체를 선택하거나, @Primary를 이용해서 식별할 수 있도록 해준다. 이에 대한 내용은 전에 작성한 포스트를 참고하자.
@Autowired 어노테이션을 붙인 타입에 해당하는 빈이 존재하지 않으면 오류가 발생한다했다. 오류가 나지 않는 방법은 무엇일까? 크게 3가지 방법이 있다.
첫번째는, @Autowired 어노테이션에 required 속성을 false로 지정하면 매칭되는 빈이 없어도 익셉션이 발생하지 않는다.
public class ImageUpdateService {
private ImageDao imageDao;
...
@Autowired(required = false)
public void setImageDao(ImageDao imageDao) {
this.imageDao = imageDao;
}
}
이렇게 required 속성을 false로 하면 매칭되는 빈이 없어도 익셉션이 발생하지 않고 메서드를 실행하지 않는다. 그렇게 되면 imageDao는 null이 될 것이다.
이 경우 문제가 되는 것이 null인데, 스프링 5버전 이후부터는 required 속성을 false로 하는 것보다 Optional을 사용하는 것이 좋다. 이 방법이 2번째 방법인데 의존성을 주입받을 객체가 null이 되면 NullPointException이 발생할 수 있다. 그래서 Optional을 활용해서 일치하는 빈이 존재하지 않아도 오류가 발생하지 않고 해당하는 빈이 있으면 의존성을 주입한다.
public class ImageRegisterService {
private ImageDao imageDao;
...
@Autowired
public void setImageDao(Optional<ImageDao> imageDaoOptional) {
if (imageDaoOptional.isPresent()) {
this.imageDao = imageDaoOptional.get();
} else {
imageDao = null;
}
}
}
마지막 방법은 @Nullable을 활용하는 방법이다. @Nullable 어노테이션을 의존 주입 대상 파라미터에 붙이면, 스프링 컨테이너는 해당 메서드를 호출할 때 자동 주입할 빈이 존재하면 해당 빈을 인자로 전달하고, 존재하지 않으면 null을 전달한다.
public class ImageUpdateService {
private ImageDao imageDao;
...
@Autowired
public void setImageDao(@Nullable ImageDao imageDao) {
this.imageDao = imageDao;
}
}
그러면 @Autowired의 required 속성을 false로 할 때와 @Nullable 어노테이션의 차이점은 required가 false면 해당 메서드를 호출 자체를 안하고 @Nullable은 메서드가 호출은 된다는 점이다. 이 방법은 필드나 메서드 모두 같은 방식으로 사용해도 된다.
만약 생성자에서 의존성 미리 주입했다면 어떻게 될까?
public class ImageUpdateService {
private ImageDao imageDao;
...
public ImageUpdateService() {
this.imageDao = new ImageDao();
}
@Autowired
public void setImageDao(@Nullable ImageDao imageDao) {
this.imageDao = imageDao;
}
}
위의 코드를 보면 해당 객체를 생성할 때 의존 주입 객체를 초기화 했다. @Nullable 붙은 세터 메서드를 호출한다. 만약 세터 파라미터가 null이라면 생성자에서 초기화한 의존 객체 ImageDao가 null이 된다. 만약 @Nullable 대신 @Autowired의 required가 false면 호출자체를 안하기 때문에 초기화한 의존 객체는 그대로 유지된다. 이러한 차이점이 있기 때문에 유의해서 사용해야한다.
만약 직접 의존성을 주입한 객체에 @Autowired가 붙어 있으면 어떻게 될까? 이럴 경우는 @Autowired 어노테이션이 붙은 타입의 객체로 의존성이 주입된다. 따라서 @Autowired 어노테이션 붙어있는 것이 직접 의존 객체를 주입한 것보다 우선순위가 높다. 따라서 자동 주입을 하는 코드와 수동으로 주입하는 코드가 섞어 있으면 주입을 제대로 하지 않아서 NullPointException이 발생했을 때 오류 지점을 찾기가 어려울 수 있다. 그래서 자동 주입을 사용할 거면 자동으로 하고 수동으로 할거면 수동으로 의존성을 주입하자.