Spring에서 annotation 기반의 DI를 제공한 것은 2.5부터이며, 그걸 가능하게 해준 annotation이 바로 @Autowired
이다. 이걸 활용해 bean 안에 bean을 주입하는 것이 가능해진다.
이 글에서는 이 @Autowired
및 관련 요소들의 활용법에 대해서 배워본다.
@Autowired
Annotations먼저 Spring bean autowiring이란 상호협력하는 의존관계를 가지는 bean들을 Spring의 configuration 파일에 선언해서 Spring IoC container이 그 관계를 관리하게 해주는 기능이다.
그럴려면 먼저 configuration 파일을 설정하고, IoC container이 의존관계를 가지는 bean들을 뭘 가지고 생성할지, 그리고 뭘 가지고 의존관계를 파악할지를 다 정해야 한다. 그리고 이를 수행하는 방법은 자주 봤는데, @Configuration
과 @ComponentScan
이다. xml의 경우 <context:annotation-config>
를 사용.
@Configuration
@ComponentScan("com.baeldung.autowire.sample")
public class AppConfig {}
여기서 Spring Boots는 더 나아가 @SpringBootApplication
이라는 annotation을 만들게 되었다. 아마 Spring 공부를 처음 할 때 이 annotation을 더 많이 봤을텐데 여기서 포함하는 것은 @Configuration
, @EnableAutoConfiguration
, @ComponentScan
이다.
그래서 밑과 같이 코드를 짜면 자동으로 App
이 속한 package와 그 sub-package의 component들을 찾아내서 bean으로 다 만들고 이걸 Spring의 ApplicationContext
에다가 추가한다. 그리고 해당 bean들은 @Autowired
를 통해 주입이 가능한 상태가 된다! 뿐만 아니라 application에서 필요로 하는 bean들을 자동으로 구성하는 기능도 있다.
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
@EnableAutoConfiguration
앞에서 application에서 필요로 하는 bean을 자동으로 구성하는 기능도 있다고 했는데 그걸 담당하는 annotation이다. 정확히는 ApplicationContext
를 자동으로 구성한다는 말이 맞을거다.
이 구성에 판단이 되는 요소는
즉 @EnableAutoConfiguration
은 이것들을 종합해서 필요로 하는 bean들을 자동으로 구성하는 것이다. 예를들어 HSQLDB이라는 DBMS를 dependency에 넣었는데 database의 연결과 관련된 bean을 하나도 안 정의했다? 그러면 자동으로 구성해준다.
그러면 뭐 앞에 배운 @Configuration
이랑 @Bean
, @ComponentScan
같은거 다 필요없고 그냥 @EnableAutoConfiguration
하나로 해결하면 코딩 끝! 아니냐고 할 수 있다. 사...실 일단 @Configuration
이 아예 배척되는건 아닌게 @Configuration
이 있는 곳에다가 @EnableAutoConfiguration
을 보통 올리기 때문이고 또 그거랑 별개로 저 3개의 차별점이 있다.
이게 지 나름대로의 규칙 하에 자동으로 구성하는 것이다 보니 자동으로 구성한 녀석이 맘에 안들거나, 필요로 하는 것이 자동 구성에 없는 경우가 있을 수 있다. 그러면? 우리가 관련 환경 설정을 해줘야 한다. 그리고 관련 bean도 만들어줘야 한다. 관련 component들을 만들어줘야 하고 그 직접 만든 component들을 Spring측에서 탐지할 수 있어야 한다. 이러한 이유로 3개의 annotation은 아직 필요하긴 하다. 우리가 원하는 환경설정 관련 bean을 만드는 method를 필요로 할 수 있고, 또 여러 추가 기능을 담당하는 bean들이 존재할 수 있고 이것을 Spring 측에서 다 탐지해낼 수 있어야 하기 때문이다.
참고로 직접 만든 요소가 자동으로 구성하는 요소를 대체하는 용도로 만들어진 경우, Spring측에서 알아서 직접 만든 요소로 대체해준다는 점 참고
위랑 별개로 우리가 따로 자동 구성 요소를 대신해서 만든 것도 없고, 그냥 일부 자동 구성하는 요소가 필요가 없다고 생각하는 경우 그것들 생성을 방지하는 방법이 있다. exclude
라는 attribute를 전달하면 된다.
import org.springframework.boot.autoconfigure.*;
import org.springframework.boot.autoconfigure.jdbc.*;
import org.springframework.context.annotation.*;
@Configuration
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class MyConfiguration {
}
아니면 spring.autoconfigure.exclude
라는 property를 application.properties
에다가 추가해서 제외할 애들 목록을 다 집어넣는 것도 가능하다. 앞의거랑 이거랑 혼용해도 된다.(...)
어떤 요소가 뭔 dependency 때문에 추가되는지 알고 싶으면 --debug
를 사용해서 애플리케이션을 실행하면 된다.
이건 Java 문법과 좀 관련되어 있는 내용이다.
엄청 대단한건 아니고, 프로그램에서 사용할, 혹은 컴파일에서 사용할 class랑 라이브러리 파일(JAR 파일)들을 어디서 찾는지 알려주는 녀석들이다.
가장 기본적인 classpath는 우리의 jdk library가 있는 classpath다. 하지만 그거랑 더불어 gradle이랑 maven의 pom.xml
이나 build.gradle
에 있는 dependency들도 전부 다 classpath에 해당된다. 그래서 우리가 spring 관련 dependency를 추가할 때 @EnableAutoConfiguration
이 다 파악할 수 있는 것이다.
@Autowired
@Autowired
를 통해 주입을 하려면 어디다가 사용하면 될까?@Autowired
on Properties먼저 property에 주입하는 것이 가능하다.
밑과 같은 bean이 있다고 해보자. 이름은 fooFormatter
@Component("fooFormatter")
public class FooFormatter {
public String format() {
return "foo";
}
}
fooFormatter
에 아까 만든 bean이 주입된다.@Component
public class FooService {
@Autowired
private FooFormatter fooFormatter;
}
@Autowired
on Setters@autowired
를 넣는 것도 가능하다. 앞과 동일한 효과를 보이는 예제.public class FooService {
private FooFormatter fooFormatter;
@Autowired
public void setFormatter(FooFormatter fooFormatter) {
this.fooFormatter = fooFormatter;
}
}
@Autowired
on Constructors마지막으로, 생성자에 @Autowired
를 넣는 것도 가능하다. 앞과 동일한 효과를 보이는 예제.
이것에 관해서는 이전 글에 자세히 다룬 적이 있다.
public class FooService {
private FooFormatter fooFormatter;
@Autowired
public FooService(FooFormatter fooFormatter) {
this.fooFormatter = fooFormatter;
}
}
@Autowired
and Optional Dependencies어떤 bean이 만들어질때 그 녀석이 @Autowired
에 해당하는 영역이 있으면 그거랑 관련된 dependency bean을 만드는 방법이 무조건 존재해야 한다. 안 그러면 exception이 발생한다.
예를들어 밑의 경우 FooDAO
에 해당하는 bean이 없으면 오류가 나온다.
public class FooService {
@Autowired()
private FooDAO dataAccessor;
}
public class FooService {
@Autowired(required = false)
private FooDAO dataAccessor;
}
@Autowired
는 기본적으로 type을 기반으로 어떤 bean을 어디에 주입해야 할지 판단한다. 그런데 만약 같은 type을 가지는 bean이 2개가 있다면? 오류가 난다.(...)
즉 2개 이상의 후보가 나오면 오류가 나온다는 것인데 이 때 이를 명시하는 방법이 여러가지가 존재한다.
@Qualifier
먼저 @Qualifier
을 사용하는 방식이다. 이거에 대해 깊이 다루는건 나중에 하고... 어떻게 사용하는지만 이번에 알아보자.
먼저 Formatter
type을 가진 bean을 2개 만들어보자.
@Component("fooFormatter")
public class FooFormatter implements Formatter {
public String format() {
return "foo";
}
}
@Component("barFormatter")
public class BarFormatter implements Formatter {
public String format() {
return "bar";
}
}
Formatter
bean을 주입받는 FooService
를 만들어보자.public class FooService {
@Autowired
private Formatter formatter;
}
@Qualifier
을 이렇게 사용하면 첫번째 Foormatter
bean을 주입한다.public class FooService {
@Autowired
@Qualifier("fooFormatter")
private Formatter formatter;
}
@Qualifier
을 사용시 이름을 기반으로 찾는다는 것을 알 수 있다. 실제로 @Qualifier
이 기본으로 사용하는 값이 이름에 해당한다. 그런데 굳이 이름으로만 해야할까? 우리만의 @Qualifier
을 만들 순 없을까? 가능하다. 밑과 같이 하면 된다.@Qualifier
@Target({
ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface FormatterType {
String value();
}
@FormatterType("Foo")
@Component
public class FooFormatter implements Formatter {
public String format() {
return "foo";
}
}
@FormatterType("Bar")
@Component
public class BarFormatter implements Formatter {
public String format() {
return "bar";
}
}
formatter
에 주입되게 된다.@Component
public class FooService {
@Autowired
@FormatterType("Foo")
private Formatter formatter;
}
@Target
저게 구체적으로 어떻게 동작하는건지 하나하나 알아보자.
먼저 @Target
은 Java에서 제공하는 annotation으로 annotation을 적용할 수 있는 곳을 제한시키는 것이다. 위의 경우 저 4가지로 제한을 하는 것이며 자세한 내용은 이 글 참고.
@Retention
역시나 annotation 설정과 관련해 제공되는 annotation으로 Java에서 만들었다.
이 글의 댓글에 매우 잘 설명되어 있다. 궁금하면 읽어보자. 딱히 추가 설명이 필요 없을 정도...
그러면 위의 경우 RetentionPolicy.RUNTIME
으로 설정된 이유는 스프링에서 injection을 할 때 @Qualifier
annotation을 참고하기 때문이라고 추정하는게 가능하다.
@Interface
annotation을 만드는데 사용되는 annotation이다. 위치도 저기에 있는 것을 보면 대충 느낄 수 있죠.
이 글 참고. 여기서 알 수 있는건 위의 두 annotation이 이 @Interface
를 위한 meta annotation이라는 것이다.
옆에 있는 이름은 해당 annotation이 가질 이름이다. 여기서는 FormatterType
임을 알 수 있다. 실제로 우리가 사용한 annotation들도 다 FormatterType
임을 볼 수 있다.
그 밑에 함수같이 생긴건 사실 함수는 아니고, 해당 값을 반환하는 annotation의 attribute라고 생각하면 된다. 여기서 개수가 1개이고 이름이 value
이기 때문에 annotation에 값을 집어넣을 때 무슨 값을 집어넣는지 명시할 필요가 없다.
그러면 저 값이 무엇에 쓰이는가? 당연히 무슨 bean을 inject할지 정하는데 쓰이는 것이다. 어떻게 해당 annotation이 그 용도로 쓰이는지를 알 수 있는거냐고요? @Qualifier
annotation이 FormatterType
에 달려있기 때문입니다. 즉 사용자 설정 @Qualifier
을 만들 때도 맨 처음에 @Qualifier
annotation을 달아야 한다.
위의 경우에는 기본 @Qualifier
이랑 차이를 별로 보이지 않는 custom annotation이다. 하지만 밑과 같이 annotation을 만들고 코딩하면 특정 parameter에 주입될 bean을 만드는 것이 가능하다. 밑의 경우 highPerformanceDevService
에서 만든 bean이 ConsumerService
bean의 service
field에 주입되게 된다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface ProfiledEnvironment {
String environmentType();
String performance();
}
@Configuration
public class AppConfig {
@Bean
@ProfiledEnvironment(environmentType = "dev", performance = "high")
public Service highPerformanceDevService() {
return new HighPerformanceService();
}
@Bean
@ProfiledEnvironment(environmentType = "prod", performance = "low")
public Service lowPerformanceProdService() {
return new LowPerformanceService();
}
}
@Component
public class ConsumerService {
private Service service;
@Autowired
public ConsumerService(@ProfiledEnvironment(environmentType = "dev", performance = "high") Service service) {
this.service = service;
}
}
사실 굳이 위의 annotation들을 사용하지 않아도 후보군을 판별할 때 애초에 type'만' 보진 않는다. type이 동일한 bean이 2개 이상이면 그 다음에 자동으로 이름을 보고 판단을 한다(...)
그래서 밑의 코드도 사실 5.1과 같은 환경에서 첫번째 bean을 inject 받는다. 다만 이름을 진짜로 똑같이 해야 하기 때문에 번거롭고 읽는 사람이 헷갈릴 수 있다는 특징이 있다.
public class FooService {
@Autowired
private Formatter fooFormatter;
}