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

1. Overview

  • Spring에서 annotation 기반의 DI를 제공한 것은 2.5부터이며, 그걸 가능하게 해준 annotation이 바로 @Autowired이다. 이걸 활용해 bean 안에 bean을 주입하는 것이 가능해진다.

  • 이 글에서는 이 @Autowired 및 관련 요소들의 활용법에 대해서 배워본다.

2. Enabling @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

  • javadocs

  • 이 글을 많이 참고했다.

  • 앞에서 application에서 필요로 하는 bean을 자동으로 구성하는 기능도 있다고 했는데 그걸 담당하는 annotation이다. 정확히는 ApplicationContext를 자동으로 구성한다는 말이 맞을거다.

  • 이 구성에 판단이 되는 요소는

    • classpath
    • 사용자가 정의한 bean들
  • @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를 사용해서 애플리케이션을 실행하면 된다.

classpath

  • 이건 Java 문법과 좀 관련되어 있는 내용이다.

  • 엄청 대단한건 아니고, 프로그램에서 사용할, 혹은 컴파일에서 사용할 class랑 라이브러리 파일(JAR 파일)들을 어디서 찾는지 알려주는 녀석들이다.

  • 가장 기본적인 classpath는 우리의 jdk library가 있는 classpath다. 하지만 그거랑 더불어 gradle이랑 maven의 pom.xml이나 build.gradle에 있는 dependency들도 전부 다 classpath에 해당된다. 그래서 우리가 spring 관련 dependency를 추가할 때 @EnableAutoConfiguration이 다 파악할 수 있는 것이다.

3. Using @Autowired

  • 그러면 @Autowired를 통해 주입을 하려면 어디다가 사용하면 될까?

3.1 @Autowired on Properties

  • 먼저 property에 주입하는 것이 가능하다.

  • 밑과 같은 bean이 있다고 해보자. 이름은 fooFormatter

@Component("fooFormatter")
public class FooFormatter {
    public String format() {
        return "foo";
    }
}
  • 그리고 밑과 같은 component를 만들면 fooFormatter에 아까 만든 bean이 주입된다.
@Component
public class FooService {  
    @Autowired
    private FooFormatter fooFormatter;
}

3.2 @Autowired on Setters

  • setter에 @autowired를 넣는 것도 가능하다. 앞과 동일한 효과를 보이는 예제.
public class FooService {
    private FooFormatter fooFormatter;
    @Autowired
    public void setFormatter(FooFormatter fooFormatter) {
        this.fooFormatter = fooFormatter;
    }
}

3.3 @Autowired on Constructors

public class FooService {
    private FooFormatter fooFormatter;
    @Autowired
    public FooService(FooFormatter fooFormatter) {
        this.fooFormatter = fooFormatter;
    }
}

4. @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; 
}

5. Autowire Disambiguation

  • @Autowired는 기본적으로 type을 기반으로 어떤 bean을 어디에 주입해야 할지 판단한다. 그런데 만약 같은 type을 가지는 bean이 2개가 있다면? 오류가 난다.(...)

  • 즉 2개 이상의 후보가 나오면 오류가 나온다는 것인데 이 때 이를 명시하는 방법이 여러가지가 존재한다.

5.1 Autowiring by @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;
}

5.2 Autowiring by Custom Qualifier

  • 위에 보면 @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";
    }
}
  • 그러면 밑과 같이 할 경우 첫번째 bean이 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;
    }
}

5.3 Autowiring by Name

  • 사실 굳이 위의 annotation들을 사용하지 않아도 후보군을 판별할 때 애초에 type'만' 보진 않는다. type이 동일한 bean이 2개 이상이면 그 다음에 자동으로 이름을 보고 판단을 한다(...)

  • 그래서 밑의 코드도 사실 5.1과 같은 환경에서 첫번째 bean을 inject 받는다. 다만 이름을 진짜로 똑같이 해야 하기 때문에 번거롭고 읽는 사람이 헷갈릴 수 있다는 특징이 있다.

public class FooService {
  @Autowired 
  private Formatter fooFormatter; 
}
profile
안 흔하고 싶은 개발자. 관심 분야 : 임베디드/컴퓨터 시스템 및 아키텍처/웹/AI

0개의 댓글

Powered by GraphCDN, the GraphQL CDN