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에 관련 글이 있다. 관련해서 자세히 설명한 글
@ContextConfigurationApplicationContext를 구성하는데 사용할 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을 사용할 수 밖에 없다.