[Spring] 의존 자동 주입, 컴포넌트 스캔

Fortice·2021년 1월 9일
1

Spring

목록 보기
3/13
post-thumbnail

의존 자동 주입

의존 자동 주입은 스프링 설정 파일에서 객체를 주입할 때, 명시를 하지 않아도 스프링 컨테이너가 자동으로 필요한 의존 대상 객체를 찾아서 필요한 객체에게 주입해주는 기능이다. @Autowired @Resource어노테이션을 이용해 구현 가능하다.

@Autowired

정의

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public abstract @interface Autowired {
  public abstract boolean required() default true;
}

기존과 다른 점

주입하려고 하는 '객체의 타입'이 일치하는 객체를 자동으로 주입한다.
생성자나 setter를 통해 직접 의존성을 주입하고, @Bean을 통해 Bean 등록을 했다면, @Autowired는 의존성 주입을 자동으로 해주고, @Component를 통해 Bean 등록을 한다.

어노테이션 정리

계속 보면서 어노테이션에 대해 헷갈려 다시 정리하고 넘어간다.

  • @Configuration - 클래스단에 설정하며, 해당 클래스가 Bean 구성에 대한 정보를 담고있는 클래스임을 알린다. (싱글톤을 유지하게 해줌)
  • @Bean - 메소드단에 설정하며, 해당 메소드를 통해 반환되는 객체를 Bean으로 컨테이너에 등록한다. (이름은 name 속성으로 정함)
  • @Component - 클래스 단에 설정하며 해당 클래스로 생성되는 객체를 Bean으로 컨테이너에 등록한다. (이름은 value 속성으로 정함)
  • @Autowired - Component를 통해 등록한 클래스에 @Autowired로 설정한 클래스로의 의존성을 자동으로 주입해준다. (해당 클래스도 Component 설정 필요)
  • @Controller - 스프링 MVC 컨트롤러로 인식한다.
  • @Repository - 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
  • @Service - 특별한 처리를 하지 않는다. 대신 개발자들이 비즈니스 로직이 여기에 있겠구나 라고 비즈니스 계층을 인식하는데 도움이 된다.

Bean과 Component는 둘 다 빈을 등록하도록 하지만, Bean은 직접 작성하지 않은 외부 라이브러리를 등록할 때 주로 사용하고, Component는 직접 작성한 클래스를 등록할 때 사용하기 위함이라고 한다.

  • 기존 코드
public class UserDao {
    private ConnectionMaker connectionMaker;

    public UserDao(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}
public interface ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
  • @Autowired 사용
@Component
public class UserDao {
    @Autowired
    private ConnectionMaker connectionMaker;
}
@Component
public interface ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
  • 다른 방법 (정의를 보면 Target에 METHOD,CONSTRUCTOR 등 다른 타입에도 가능하다)
// 생성자 --> 생성자에서 의존성을 주입해주니 생략가능
private ConnectionMaker connectionMaker;

@Autowired  
public UserDao(ConnectionMaker connectionMaker) {
    this.connectionMaker = connectionMaker;
}
// 객체, 생성자 명시
@Autowired  
private ConnectionMaker connectionMaker;

public UserDao() {
}
public UserDao(ConnectionMaker connectionMaker) {
    this.connectionMaker = connectionMaker;
}
// Setter 사용
@Component
public class UserDao {
    private ConnectionMaker connectionMaker;
    @Autowired(required=false)
    public void setConnectionMaker(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}
// 오류나는 코드
@Autowired
private ConnectionMaker connectionMaker;
 
public UserDao(ConnectionMaker connectionMaker) {
    this.connectionMaker = connectionMaker;
}

생성자 주입과는 다르게 setter를 이용한 주입은 필요에 의해 주입을 하기 위함으로, 대상 객체까지 Bean으로 등록하는게 필수는 아니다. 만약 required=false가 없으면 해당 코드는 빨간줄이 그어진다.

정의를 보면 required 속성은 default가 true인데, 의미는 의존성 주입을 꼭 받아야한다는 것이다. 따라서 이를 false로 해주어 빈 객체로 남겨둘 수 있다.

의존 주입 대상 객체가 여러개일 경우

만약 Autowired가 설정된 ConnectionMaker 타입이 여러개면 오류가 난다. Autowired가 빈의 타입을 보고 의존 주입을 자동으로 하기 때문이다.

일단 순서를 알아보자

Bean 검색 순서 순서

  1. 타입이 같은 빈 객체를 검색한다. 한개면 그 빈 객체를 사용한다. @Qualifier가 명시되어 있을 경우, @Qualifier와 같은 값을 갖는 빈 객체여야 한다.

  2. 타입이 같은 빈 객체가 두 개 이상 존재하면, @Qualifier로 지정한 빈 객체를 찾는다. 존재하면, 그 객체를 사용한다.

  3. 타입이 같은 빈 객체가 두 개 이상 존재하고 @Qualifier가 없을 경우, 이름이 같은 빈 객체를 찾는다. 존재하면, 그 객체를 사용한다.

@Qualifier가 나오는데, 해결방법을 알아보면서 같이 공부한다.

공부를 하다보니 의존 주입에는 @Autowired, @Resource, @Inject 의 태그들이 이용된다고 한다. 내용이 끝나고 다시 공부할 것.

Bean 검색 순서

@Autowired : 타입 => 이름 => Qualifier => Fail
@Resource : 이름 => 타입 => Qualifier => Fail
@Inject : 타입 => Qualifier => 이름 => Fail

해결 방법

  1. @Primary를 Autowired 주입 대상 중 원하는 클래스에 넣는다.(중복되는 빈 중 대표를 설정하는 것)
    @Component
    @Primary
    public class myConnection implements ConnectionMaker {
    }
  2. @Qualifier를 주입하는 곳에 @Querifier('bean id')로 명시한다. 대신 해당 bean id 를 가지는 한정자가 필요하다.
@Component  
public class UserDao {
    @Autowired
    @Qualifier("yourConnection")
    private ConnectionMaker connectionMaker;
}
@Configuration
public class DaoFactory {   
    @Bean
    public UserDao userDao() {
        return new UserDao();
    }
    @Bean( name="myConnection")
    public ConnectionMaker connectionMaker() {
        return new myConnection();
    }
    @Bean( name="yourConnection")
    public ConnectionMaker connectionMakerSub() {
        return new yourConnection();
    }
}

Configuration 부분은 XML로 할 시 이렇게 가능하다.

<context:annotation-config>
    <bean id="con1" class="example.Connection">
        <qualifier value="myConnection"/>
    </bean>
    <bean id="con2" class="example.Connection"/>
        <qualifier value="yourConnection"/>
    </bean>
<context:annotation-config/>
  1. 모든 빈을 주입받기
public class UserDao {
    @Autowired
    private List<ConnectionMaker> connectionMaker;
}

컴포넌트 스캔

@ComponentScan

@Component어노테이션 및 streotype(@Service, @Repository, @Controller.)어노테이션이 부여된 Class들을 자동으로 Scan하여 Bean으로 등록해주는 역할을 하는 어노테이션

정의

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
    @AliasFor("basePackages")
    String[] value() default {};
    @AliasFor("value")
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
    
    ... (매우 많음)
    
    boolean useDefaultFilters() default true;
    Filter[] includeFilters() default {};
    Filter[] excludeFilters() default {};
    boolean lazyInit() default false;
    
    @interface Filter {...}
}

비교

  • 일단 Target 속성을 보면 TYPE으로 클래스단에 적용한다는 것을 알 수 있다.
  • 코드 비교 - Autowired 에서 했던 것처럼 주입 대상 클래스들에 @Component 만 해두고 기존 Bean의 설정을 담당하던 @Configuration 부분에 @ComponentScan만 넣으면 내용이 없어도 자동으로 된다.
  • 테스트를 해보니 Bean 등록은 자동으로 되어 getBean 할 수 있으나, 의존성 주입은 안되는 것 같다. Autowired나 다른 방법을 통해 의존성 주입이 필요할 것 같다.
  • @ComponentScan이 붙어있는 Configuration부터 스캔
  • Spring boot 프로젝트에서는 @SpringBootApplication에 이미 @Configuration@ComponentScan이 붙어있다. 따라서 @SpringBootApplication 기준으로 스캐닝이 시작된다.

해당 Application이 위치한 패키지 이하의 모든 클래스에 컴포넌트 스캔이 진행된다.

@ComponentScan
@Configuration
public class DaoFactory {   
}

중요한 속성

@ComponentScan 에는 중요한 속성들이 있다.

Scan 범위 지정

  1. basePackages

    정의를 보면 String 타입임을 알 수 있다.
    패키지 경로를 String으로 넘겨 해당 패키지로 스캔 위치를 지정한다.
    String으로 지정하여 typesafe하지 않기 때문에 2번 방법으로 지정하는 것을 추천한다.

    정의를 보면 @AliasFor이 되어있는데 이는 basePackages 를 value로 해도 상관 없다는 뜻이다.

@ComponentScan(basePackages = "com.fortice.spring")
@ComponentScan(value = "com.fortice.spring")
  1. basePackageClasses
    정의를 보면 Class<?> [] 타입임을 알 수 있다.
    class를 지정하여 해당 클래스가 위치한 곳에서 부터 모든 어노테이션을 스캔한다.
    typesafe 한 방법이다.
@ComponentScan(basePackageClasses = UserDao.class)
  1. useDefaultFilters
    defualt가 true인데, 이를 false로 바꾸면 기본적인 스캔 범위가 다 사라진다.

  2. includeFilters, excludeFilters
    useDefaultFilters로 삭제한 뒤 다시 포함/제외시키고 싶은 빈 타입을 지정할 수 있다.
    typeclasses or 'pattern'을 지정한다.

    type

    FilterType.ANNOTATION - 어노테이션 기준, classes
    FilterType.ASPECTJ - AspectJ 패턴 기준, pattern (ex. "com.fortice.")
    FilterType.ASSIGNABLE_TYPE - 클래스 기준, classes
    FilterType.REGEX - 정규식 기준, pattern (ex. ".
    component.*")
    FilterType.CUSTOM

@ComponentScan(
    basePackages = "com", 
    useDefaultFilters = false,
    includeFilters = {
        @Filter(
            type = FilterType.ANNOTATION, 
            classes = {Component.class, Repository.class, Service.class, Controller.class}
        )
    }
  1. lazyInit
    기본적으로 false로 ComponentScan을 통해 찾은 클래스들은, 바로 초기화되어 컨텍스트에 빈으로 등록된다. 하지만 lazyInit를 true로 하면, 실제 해당 클래스가 사용될 때 초기화 한다.
profile
서버 공부합니다.

0개의 댓글