DI, 즉 dependency injection이 뭔지는 알고 있을 것이라고 가정. 이전 글에서 @Autowired
기반의 DI에 대해 얘기했었다. 그런데 사실 DI는 @Autowired
만으로 할 수 있는 것이 아니다. @Resource
와 @Inject
도 있다.
이들 모두 기존의 new
keyword를 써가지고 어떤 class의 instance를 주입하는게 아니라 단순 선언만으로 instance를 주입한다는 공통된 특징을 가지고 있다.
그러나 @Autowired
는 Spring framework의 일부이지만, @Resource
와 @Inject
는 Java extension package에 속해 있다. 이번 글은 이들을 어떻게 사용하는지, 뭐가 다른지, 특정 상황에서 뭘 써야 하는지 등에 대해 알아볼거다. 활용 상황은 공통적으로 integration testing을 하기 위해 injection annotation을 사용해야 하는 경우
참고로 이들 셋 모두 field injection과 setter injection의 방법으로 크게 나눠진다.
반복적인 내용이 많기에 빠른 결론을 보고 싶으면 5번으로 이동하자.
Jakarta EE (Java EE) 소속. JSR-250 annotation 모음의 일부다. 2006년 즈음에 최종 발표가 된 annotation들 중 하나.
다음과 같은 방식으로 field/setter 모두에 inject가 가능하다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestResourceNameType.class)
public class FieldResourceInjectionIntegrationTest {
@Resource(name="namedFile")
private File defaultFile;
@Test
public void givenResourceAnnotation_WhenOnField_ThenDependencyValid(){
assertNotNull(defaultFile);
assertEquals("namedFile.txt", defaultFile.getName());
}
}
defaultFile
에 namedFile
이라는 이름을 가지는 bean을 주입하는 것이다. 해당 bean은 다음 class에서 등장한다.@Configuration
public class ApplicationContextTestResourceNameType {
@Bean(name="namedFile")
public File namedFile() {
File namedFile = new File("namedFile.txt");
return namedFile;
}
}
namedFile
이라는 이름의 bean을 생성하는 것이란건 다 알거고, 앞의 test 코드에 대해 좀 설명하겠다.@RunWith
일단 여기서 사용하는 테스트 관련 패키지는 JUnit
이다. 보통 unit test를 할 때 자주 쓰이는 프레임워크고 Spring에서도 자주 등장한다.
그런데 유의해야하는게 지금 코드에서 사용하는 JUnit
은 ~JUnit4이고 현재 주로 쓰이는 JUnit
은 JUnit5이라는 것이다. 그리고 5에는 @Runwith
가... 없는건 아닌데 @ExtendWith
를 쓰도록 권장된다. @Runwith
는 호환성 때문에 유지.
간략한 용도랑 현재 대체된 annotation은Baeldung에 관련 글이 있다. 관련해서 자세히 설명한 글
@ContextConfiguration
ApplicationContext
를 구성하는데 사용할 class랑 이를 load하는 것을 결정하는 loader을 지정하는 annotation. 보통 loader은 넣을 필요 없다고 하지만 여기서는 넣었다.
classes
에 class가 아니 파일을 넣는 것도 가능하지만, 그건 다른 이야기. 직관적인 내용이지만 classes
에 넣는 class는 @Configuration
annotation을 가져야 한다.
@Test
단순히 테스트용 method임을 나타내는데 사용되는 annotation이다.
즉 결국 저 코드는 매우 간단한, namedFile
이라는 bean의 이름이 namedFile.txt
인지를 확인하는 테스트이며 위처럼 bean의 이름을 namedFile
로 하고 @Resource
에서도 그 이름을 지정해가지고 해당 bean을 정확히 주입받은 상황이다. 만약 둘의 이름이 서로 다르면 오류가 나온다.
@Resource
가 달려있는 부분을 다음과 같이 바꾸면 된다.@Resource
private File defaultFile;
이는 해당 context에서 File
에 해당하는 bean이 namedFile
bean밖에 없기 때문. 만약 저 타입을 String
같은 다른 타입으로 바꾸면 오류가 나온다.
참고로 타입 기반 주입의 우선순위는 이름보다 한 단계 더 낮다.
@Configuration
class가 있다고 해보자.@Configuration
public class ApplicationContextTestResourceQualifier {
@Bean(name="defaultFile")
public File defaultFile() {
File defaultFile = new File("defaultFile.txt");
return defaultFile;
}
@Bean(name="namedFile")
public File namedFile() {
File namedFile = new File("namedFile.txt");
return namedFile;
}
}
dependency1
에 defaultfile
에 해당하는 bean이 주입되고 dependency2
에 namedfile
에 해당하는 bean이 주입되는지를 확인하고 있다.@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestResourceQualifier.class)
public class QualifierResourceInjectionIntegrationTest {
@Resource
private File dependency1;
@Resource
private File dependency2;
@Test
public void givenResourceAnnotation_WhenField_ThenDependency1Valid(){
assertNotNull(dependency1);
assertEquals("defaultFile.txt", dependency1.getName());
}
@Test
public void givenResourceQualifier_WhenField_ThenDependency2Valid(){
assertNotNull(dependency2);
assertEquals("namedFile.txt", dependency2.getName());
}
}
이 상태로 코드를 돌리면 NoUniqueBeanDefinitionException
이 나온다. 이유는 File
에 해당하는 bean이 2개고, dependency1
이랑 dependency2
둘 다 둘 중 어떤 bean을 주입받아야 할지 모르기 때문.
그러나 밑과 같이 Qualifier
을 사용하면 중비이 가능해진다.
@Resource
@Qualifier("defaultFile")
private File dependency1;
@Resource
@Qualifier("namedFile")
private File dependency2;
defaultFile
이랑 namedFile
로 해도 되지 않냐라고 할 수 있고 실제로 가능하지만 field의 이름을 다르게 해서 주입할 수 있다는 것이 이점이니...@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestResourceNameType.class)
public class MethodResourceInjectionIntegrationTest {
private File defaultFile;
@Resource(name="namedFile")
protected void setDefaultFile(File defaultFile) {
this.defaultFile = defaultFile;
}
@Test
public void givenResourceAnnotation_WhenSetter_ThenDependencyValid(){
assertNotNull(defaultFile);
assertEquals("namedFile.txt", defaultFile.getName());
}
}
defaultFile
에 namedFile.txt
라는 이름을 가진 파일이 주입되었는지를 확인하고 있다. Bean 정의가 2.1.1과 동일하다고 가정했을 때 위와 같이 하면 테스트는 통과한다.@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestResourceNameType.class)
public class MethodByTypeResourceIntegrationTest {
private File defaultFile;
@Resource
protected void setDefaultFile(File defaultFile) {
this.defaultFile = defaultFile;
}
@Test
public void givenResourceAnnotation_WhenSetter_ThenValidDependency(){
assertNotNull(defaultFile);
assertEquals("namedFile.txt", defaultFile.getName());
}
}
arbDependency
파일이 namedFile
이름을 가진 파일이 주입되었고 anotherArbDependency
파일이 defaultFile
이름을 가진 파일이 주입되는지 확인.)@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestResourceQualifier.class)
public class MethodByQualifierResourceIntegrationTest {
private File arbDependency;
private File anotherArbDependency;
@Test
public void givenResourceQualifier_WhenSetter_ThenValidDependencies(){
assertNotNull(arbDependency);
assertEquals("namedFile.txt", arbDependency.getName());
assertNotNull(anotherArbDependency);
assertEquals("defaultFile.txt", anotherArbDependency.getName());
}
@Resource
@Qualifier("namedFile")
public void setArbDependency(File arbDependency) {
this.arbDependency = arbDependency;
}
@Resource
@Qualifier("defaultFile")
public void setAnotherArbDependency(File anotherArbDependency) {
this.anotherArbDependency = anotherArbDependency;
}
}
역시나 Jakarta EE (Java EE) 소속 annotation으로 JSR-330 annotation 모음의 일부다. 2009년 즈음에 최종 발표가 된 annotation들 중 하나.
다음과 같은 방식으로 field/setter 모두에 inject가 가능하다.
@Resource
와의 가장 큰 차이점은 타입 기반 주입의 우선순위가 제일 높다는 것이다.
밑처럼 bean 생성 및 테스트 코드를 작성하면 테스트가 통과하는데, ArbitraryDependency
에 해당하는 bean이 @Component
가 선언되어서 만들어지는 Arbitrary Dependency
라는 label을 가진 ArbitraryDependency
밖에 없기 때문이다. 이름이 달라도 잘 inject되고 있는 것을 볼 수 있다. 반대로 타입을 다르게 하면 오류가 나온다.
@Component
public class ArbitraryDependency {
private final String label = "Arbitrary Dependency";
public String toString() {
return label;
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestInjectType.class)
public class FieldInjectIntegrationTest {
@Inject
private ArbitraryDependency fieldInjectDependency;
@Test
public void givenInjectAnnotation_WhenOnField_ThenValidDependency(){
assertNotNull(fieldInjectDependency);
assertEquals("Arbitrary Dependency",
fieldInjectDependency.toString());
}
}
ArbitraryDependency
bean이 하나 더 있다고 해보자.public class AnotherArbitraryDependency extends ArbitraryDependency {
private final String label = "Another Arbitrary Dependency";
public String toString() {
return label;
}
}
@Qualifier
이 필요해짐. 밑의 테스트는 2개의 ArbitraryDependency
bean을 주입받는다. 어떤 field가 어떤 bean을 주입받는지를 @Qualifier
로 구별. 이를 사용하지 않으면 오류가 나온다.@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestInjectQualifier.class)
public class FieldQualifierInjectIntegrationTest {
@Inject
@Qualifier("defaultFile")
private ArbitraryDependency defaultDependency;
@Inject
@Qualifier("namedFile")
private ArbitraryDependency namedDependency;
@Test
public void givenInjectQualifier_WhenOnField_ThenDefaultFileValid(){
assertNotNull(defaultDependency);
assertEquals("Arbitrary Dependency",
defaultDependency.toString());
}
@Test
public void givenInjectQualifier_WhenOnField_ThenNamedFileValid(){
assertNotNull(defaultDependency);
assertEquals("Another Arbitrary Dependency",
namedDependency.toString());
}
}
@Named
라는 annotation을 사용한다는 특징이 있다. ArbitraryDependency
해당 class의 이름이 FieldByNameInjectIntegrationTest
의 field이름이랑 다르면 오류가 나온다.public class YetAnotherArbitraryDependency extends ArbitraryDependency {
private final String label = "Yet Another Arbitrary Dependency";
public String toString() {
return label;
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestInjectName.class)
public class FieldByNameInjectIntegrationTest {
@Inject
@Named("yetAnotherFieldInjectDependency")
private ArbitraryDependency yetAnotherFieldInjectDependency;
@Test
public void givenInjectQualifier_WhenSetOnField_ThenDependencyValid(){
assertNotNull(yetAnotherFieldInjectDependency);
assertEquals("Yet Another Arbitrary Dependency",
yetAnotherFieldInjectDependency.toString());
}
}
@Inject
랑 매우 유사하지만 Spring framework에서 제공한다는 차별점이 있다.
여기는 이전 글에서 많이 언급된 내용이라 간단하게만 언급했다. 이전 부분도 간단하게 언급한것 같긴한데
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestAutowiredType.class)
public class FieldAutowiredIntegrationTest {
@Autowired
private ArbitraryDependency fieldDependency;
@Test
public void givenAutowired_WhenSetOnField_ThenDependencyResolved() {
assertNotNull(fieldDependency);
assertEquals("Arbitrary Dependency", fieldDependency.toString());
}
}
@Configuration
public class ApplicationContextTestAutowiredType {
@Bean
public ArbitraryDependency autowiredFieldDependency() {
ArbitraryDependency autowiredFieldDependency =
new ArbitraryDependency();
return autowiredFieldDependency;
}
}
@Inject
와 매우 유사.@Configuration
public class ApplicationContextTestAutowiredQualifier {
@Bean
public ArbitraryDependency autowiredFieldDependency() {
ArbitraryDependency autowiredFieldDependency =
new ArbitraryDependency();
return autowiredFieldDependency;
}
@Bean
public ArbitraryDependency anotherAutowiredFieldDependency() {
ArbitraryDependency anotherAutowiredFieldDependency =
new AnotherArbitraryDependency();
return anotherAutowiredFieldDependency;
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestAutowiredQualifier.class)
public class FieldQualifierAutowiredIntegrationTest {
@Autowired
@Qualifier("autowiredFieldDependency")
private FieldDependency fieldDependency1;
@Autowired
@Qualifier("anotherAutowiredFieldDependency")
private FieldDependency fieldDependency2;
@Test
public void givenAutowiredQualifier_WhenOnField_ThenDep1Valid(){
assertNotNull(fieldDependency1);
assertEquals("Arbitrary Dependency", fieldDependency1.toString());
}
@Test
public void givenAutowiredQualifier_WhenOnField_ThenDep2Valid(){
assertNotNull(fieldDependency2);
assertEquals("Another Arbitrary Dependency",
fieldDependency2.toString());
}
}
여기서부터 갑자기 희한한 조건이 붙는데, 바로 밑처럼 @ComponentScan
이 달려 있는 class가 필요하다는 것이다. 이유는 @Component
를 가지고 주입할 bean을 만들어야 하기 때문이다.
기존에는 @Configuration
이 달려있는 class에서 만든 bean을 주입 했는데 @Component
를 활용해야만 @Autowire
의 이름 기반 DI를 사용할 수 있어서 이렇게 구성을 바꾼 것이다.
그러면 @Component
랑 @Configuration
을 같이 사용하면 되는거 아니냐고 할 수 있다. 그런데 이는 별로 추천 안하는게 용도 차이가 확실히 다른것도 있고 @Configuration
에서 애초에 @Component
가 포함되어 있기 때문 실제로도 용도 차이 때문에 둘을 같이 쓰는 일이 권장되지 않는걸 생각하면 여기처럼 예제를 만드는게 합리적이다.
이 점 말고는 크게 유의할만한 부분은 없다. @Component
에 있는 value
의 이름이 주입을 받으려는 field의 이름과 동일해야 주입이 제대로 이루어진다 정도.
@Configuration
@ComponentScan(basePackages={"com.baeldung.dependency"})
public class ApplicationContextTestAutowiredName {
}
@Component(value="autowiredFieldDependency")
public class ArbitraryDependency {
private final String label = "Arbitrary Dependency";
public String toString() {
return label;
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
loader=AnnotationConfigContextLoader.class,
classes=ApplicationContextTestAutowiredName.class)
public class FieldAutowiredNameIntegrationTest {
@Autowired
private ArbitraryDependency autowiredFieldDependency;
@Test
public void givenAutowired_WhenSetOnField_ThenDependencyResolved(){
assertNotNull(autowiredFieldDependency);
assertEquals("Arbitrary Dependency",
autowiredFieldDependency.toString());
}
}
@Inject
나 @Autowire
을 사용하는게 좋다. 이유는 단 하나의 class가 application의 행동양식을 결정하면 그 type의 class를 애플리케이션의 여러 곳에서 사용한다는 뜻인데, 나중에 애플리케이션에 변동사항이 생겨서 그 class가 변동이 되었을 때 타입 기반으로 빠르게 바뀔 수 있도록 하기 위해서다. 저 두 annotation이 타입 기반이 우선이어서 이 부분에서 적합하다.@Resource
를 사용하는게 좋다. 이유는 복잡할수록 class 이름을 기반으로 무슨 설정을 나타내는지 파악하기 힘들며 이름이 훨씬 더 직관적이기 때문이다.@Autowire
을 사용 못하니 @Inject
나 @Resource
를 사용할 수 밖에 없다.@Inject
나 @Resource
를 사용 못하니 @Autowire
을 사용할 수 밖에 없다.