Baeldung - Wiring in Spring: @Autowired, @Resource and @Inject

sycho·2024년 3월 20일
0

Baeldung의 이 글을 정리 및 추가 정보를 넣은 글입니다.

1. Overview

  • DI, 즉 dependency injection이 뭔지는 알고 있을 것이라고 가정. 이전 글에서 @Autowired 기반의 DI에 대해 얘기했었다. 그런데 사실 DI는 @Autowired만으로 할 수 있는 것이 아니다. @Resource@Inject도 있다.

  • 이들 모두 기존의 new keyword를 써가지고 어떤 class의 instance를 주입하는게 아니라 단순 선언만으로 instance를 주입한다는 공통된 특징을 가지고 있다.

  • 그러나 @Autowired는 Spring framework의 일부이지만, @Resource@InjectJava extension package에 속해 있다. 이번 글은 이들을 어떻게 사용하는지, 뭐가 다른지, 특정 상황에서 뭘 써야 하는지 등에 대해 알아볼거다. 활용 상황은 공통적으로 integration testing을 하기 위해 injection annotation을 사용해야 하는 경우

  • 참고로 이들 셋 모두 field injection과 setter injection의 방법으로 크게 나눠진다.

  • 반복적인 내용이 많기에 빠른 결론을 보고 싶으면 5번으로 이동하자.

2. The @Resource Annotation

  • javadoc

  • Jakarta EE (Java EE) 소속. JSR-250 annotation 모음의 일부다. 2006년 즈음에 최종 발표가 된 annotation들 중 하나.

  • 다음과 같은 방식으로 field/setter 모두에 inject가 가능하다.

    • 이름
    • 타입
    • qualifier

2.1 Field Injection

2.1.1 Match by Name

@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());
    }
}
  • 위의 테스트에서 defaultFilenamedFile이라는 이름을 가지는 bean을 주입하는 것이다. 해당 bean은 다음 class에서 등장한다.
@Configuration
public class ApplicationContextTestResourceNameType {

    @Bean(name="namedFile")
    public File namedFile() {
        File namedFile = new File("namedFile.txt");
        return namedFile;
    }
}
  • 첫 예시이므로 코드에 대해 간단하게 소개하도록 하겠다. bean 생성 관련 코드는 이전 글들에서 소개한 내용이 전부이므로 namedFile이라는 이름의 bean을 생성하는 것이란건 다 알거고, 앞의 test 코드에 대해 좀 설명하겠다.

@RunWith

  • 일단 여기서 사용하는 테스트 관련 패키지는 JUnit이다. 보통 unit test를 할 때 자주 쓰이는 프레임워크고 Spring에서도 자주 등장한다.

  • 그런데 유의해야하는게 지금 코드에서 사용하는 JUnit~JUnit4이고 현재 주로 쓰이는 JUnitJUnit5이라는 것이다. 그리고 5에는 @Runwith가... 없는건 아닌데 @ExtendWith를 쓰도록 권장된다. @Runwith는 호환성 때문에 유지.

  • 간략한 용도랑 현재 대체된 annotation은Baeldung에 관련 글이 있다. 관련해서 자세히 설명한 글

@ContextConfiguration

  • 관련 Spring documentation

  • ApplicationContext를 구성하는데 사용할 class랑 이를 load하는 것을 결정하는 loader을 지정하는 annotation. 보통 loader은 넣을 필요 없다고 하지만 여기서는 넣었다.

  • classes에 class가 아니 파일을 넣는 것도 가능하지만, 그건 다른 이야기. 직관적인 내용이지만 classes에 넣는 class는 @Configuration annotation을 가져야 한다.

@Test

  • 단순히 테스트용 method임을 나타내는데 사용되는 annotation이다.

  • 즉 결국 저 코드는 매우 간단한, namedFile이라는 bean의 이름이 namedFile.txt인지를 확인하는 테스트이며 위처럼 bean의 이름을 namedFile로 하고 @Resource에서도 그 이름을 지정해가지고 해당 bean을 정확히 주입받은 상황이다. 만약 둘의 이름이 서로 다르면 오류가 나온다.

2.1.2 Match by Type

  • 타입 기반도 별로 어렵진 않다. @Resource가 달려있는 부분을 다음과 같이 바꾸면 된다.
@Resource
private File defaultFile;
  • 이는 해당 context에서 File에 해당하는 bean이 namedFile bean밖에 없기 때문. 만약 저 타입을 String같은 다른 타입으로 바꾸면 오류가 나온다.

  • 참고로 타입 기반 주입의 우선순위는 이름보다 한 단계 더 낮다.

2.1.3 Match by Qualifier

  • 이번에는 예제를 좀 달리 한다. 같은 타입의 bean을 2개 생성하는 @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;
    }
}
  • 밑의 테스트를 보면 dependency1defaultfile에 해당하는 bean이 주입되고 dependency2namedfile에 해당하는 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;
  • 그냥 저 field들도 이름을 defaultFile이랑 namedFile로 해도 되지 않냐라고 할 수 있고 실제로 가능하지만 field의 이름을 다르게 해서 주입할 수 있다는 것이 이점이니...

2.2 Setter Injection

2.2.1 Match by Name

  • 기존 방식과 크게 다르지 않다. 다만 method 위에 annotation이 달려있다는 것 뿐.
@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());
    }
}
  • 보면 defaultFilenamedFile.txt라는 이름을 가진 파일이 주입되었는지를 확인하고 있다. Bean 정의가 2.1.1과 동일하다고 가정했을 때 위와 같이 하면 테스트는 통과한다.

2.2.2 Match by Type

  • 역시나 기존 방식과 크게 다르지 않다.
@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());
    }
}

2.2.3 Match by Qualifier

  • 2.1.3의 bean 생성 코드를 그대로 사용한다고 할 시 밑과 같이 하면 테스트는 통과한다. 목표랑 실제 코드 내용 또한 기존 방식과 크게 다르지 않다. (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;
    }
}

3. The @Inject Annotation

  • javadoc

  • 역시나 Jakarta EE (Java EE) 소속 annotation으로 JSR-330 annotation 모음의 일부다. 2009년 즈음에 최종 발표가 된 annotation들 중 하나.

  • 다음과 같은 방식으로 field/setter 모두에 inject가 가능하다.

    • 이름
    • 타입
    • qualifier

3.1 Field Injection

3.1.1 Match by Type

  • @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());
    }
}

3.1.2 Match by Qualifier

  • 다음과 같은 ArbitraryDependency bean이 하나 더 있다고 해보자.
public class AnotherArbitraryDependency extends ArbitraryDependency {

    private final String label = "Another Arbitrary Dependency";

    public String toString() {
        return label;
    }
}
  • 이러면 단순히 type만으로 비교가 불가능해진다. 그래서 @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());
    }
}

3.1.3 Match by Name

  • 마지막으로 이름 기반 주입. @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());
    }
}

3.2 Setter Injection

  • ...method에다가 annotation을 단다는것 말고 실질적으로 달라지는게 없다는 언급만 있다. 사실 앞의 예시들도 코드 길이만 길지 영양가가 엄청 있는 내용은 아니긴하다

4. The @Autowired Annotation

  • @Inject랑 매우 유사하지만 Spring framework에서 제공한다는 차별점이 있다.

  • 여기는 이전 글에서 많이 언급된 내용이라 간단하게만 언급했다. 이전 부분도 간단하게 언급한것 같긴한데

4.1 Field Injection

4.1.1 Match by Type

  • 여기도 타입 기반 주입의 우선순위가 제일 높다.
@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;
    }
}

4.1.2 Match by Qualifier

  • 특별히 언급할 만한 내용은 없다. @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());
    }
}

4.1.3 Match by Name

  • 여기서부터 갑자기 희한한 조건이 붙는데, 바로 밑처럼 @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());
	}
}

4.2 Setter Injection

  • field 기반 주입과 크게 차이가 없어서 글에서 생략했다.

5. Applying These Annotations

  • 사실 여기가 핵심이다. 둘이 크게 다른것도 없어보이는데 어떤 상황에서 어떤 annotation을 사용해서 주입하는게 좋을까?

5.1 Application-Wide Use of Singletons Through Polymorphism

  • application 전체 행동이 하나의 특정 인터페이스나 abstract class를 기반으로 정해지면 @Inject@Autowire을 사용하는게 좋다. 이유는 단 하나의 class가 application의 행동양식을 결정하면 그 type의 class를 애플리케이션의 여러 곳에서 사용한다는 뜻인데, 나중에 애플리케이션에 변동사항이 생겨서 그 class가 변동이 되었을 때 타입 기반으로 빠르게 바뀔 수 있도록 하기 위해서다. 저 두 annotation이 타입 기반이 우선이어서 이 부분에서 적합하다.

5.2 Fine-Grained Application Behavior Configuration Through Polymorphism

  • 반대로 애플리케이션이 여러 class들을 기반으로 복잡한 행동양식을 보이면 class 기반으로 하기보다 이름 기반으로 주입하는게 낫고, 그래서 @Resource를 사용하는게 좋다. 이유는 복잡할수록 class 이름을 기반으로 무슨 설정을 나타내는지 파악하기 힘들며 이름이 훨씬 더 직관적이기 때문이다.

5.3 Dependency Injection Should Be Handled Solely by the Jakarta EE Platform

  • Jakarta EE만을 사용해야 하면 Spring에서 제공하는 @Autowire을 사용 못하니 @Inject@Resource를 사용할 수 밖에 없다.

5.4 Dependency Injection Should Be Handled Solely by the Spring Framework

profile
안 흔하고 싶은 개발자. 관심 분야 : 임베디드/컴퓨터 시스템 및 아키텍처/웹/AI

0개의 댓글