while(1) work();
반응형

ComponentScan

만약 프로그램의 규모가 커져서 등록해야 할 빈이 수백, 수천개라면 어떻게 해야할까?

의존관계 설정파일을 관리하는데 어머어머한 시간이 소요될 것이다.

Spring context에서는 빈을 자동으로 찾아 등록(주입이 아니다)하는 Component scan 기능을 제공한다.

설정 파일에 @ComponentScan 어노테이션을 붙임으로써, Component scan을 사용할 수 있다.

package com.sample.spring;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class Config {
    //기존의 빈 등록 설정은 모두 삭제
}

Spring 컨테이너가 만들어지면 인자로 받은 설정 클래스 객체는 자동으로 빈으로 등록된다. (정확히는 CGLIB에 의해 생성된 프록시 객체가 등록된다)

이 과정에서, 설정 클래스에 @ComponentScan 어노테이션이 있으면, Component scan 기능이 동작한다.

@ComponentScan은 해당 패키지와 하위 패키지 내의 모든 클래스를 살펴보고 @Component 어노테이션이 붙어있는 클래스를 빈으로 등록한다.

이 과정에서 @Autowired 어노테이션이 붙어있는 생성자, 수정자, 필드, 메서드가 있다면 빈 생성 후 의존관계도 주입한다. (생성자의 경우 생성과 동시에 주입)

따라서 코드가 올바르게 동작하려면 빈으로 등록하고자 하는 클래스에 @Component를 붙여야 한다.

package com.sample.spring;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.sample.spring.repository.SampleRepository;

@Component
public class Application {

    @Autowired
    private SampleRepository sampleRepository;

    public void run() {
        System.out.println(sampleRepository.getLastUserId());
    }

}
package com.sample.spring.repository;

import org.springframework.stereotype.Component;

@Component
public class SampleRepositoryImpl1 implements SampleRepository {

    public int getLastUserId() {
        return 100;
    }

}
package com.sample.spring.repository;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Primary
public class SampleRepositoryImpl2 implements SampleRepository {

    public int getLastUserId() {
        return 200;
    }

}

SampleRepository 타입의 빈이 두 개 등록되기 때문에, @Primary 어노테이션을 SampleRepositoryImpl2 클래스에 붙여 우선 순위를 가지고 주입되도록 하였다.

@Service, @Repository, @Controller

@Service, @Repository, @Controller 어노테이션은 해당 클래스가 각각 서비스, 레포지토리, 컨트롤러 임을 표시하기 위해 사용하는 어노테이션이다.

각 어노테이션을 붙이면 spring에서(spring context 이외에 다른 spring projects) 해당 빈을 등록할 때 추가적인 작업을 함께 해준다.

@Repository를 붙인 클래스의 경우 데이터베이스 CheckedException이 발생하면 Spring의 UncheckedException으로 변환해주는 기능이 추가되고, @Controller를 붙인 클래스의 경우 spring web mvc에서 @RequestMapping이 가능하도록 한다.

아직까지 @Service는 아무 동작도 하지 않지만 추후 추가될 기능을 대비해, 그리고 코드의 가독성을 위해 사용한다.

이 어노테이션들은 @Component 어노테이션을 포함하기 때문에, @Component 어노테이션을 추가로 붙일 필요가 없다. 각 어노테이션의 정의는 아래와 같다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
	@AliasFor(annotation = Component.class)
	String value() default "";
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
	@AliasFor(annotation = Component.class)
	String value() default "";
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
	@AliasFor(annotation = Component.class)
	String value() default "";
}

우리의 (아무런 기능도 없는) 두 Repository도 @Repository 어노테이션으로 변경하자.

package com.sample.spring.repository;

import org.springframework.stereotype.Repository;

@Repository
public class SampleRepositoryImpl1 implements SampleRepository {

    public int getLastUserId() {
        return 100;
    }

}
package com.sample.spring.repository;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;

@Repository
@Primary
public class SampleRepositoryImpl2 implements SampleRepository {

    public int getLastUserId() {
        return 200;
    }

}

ComponentScan 설정

기본적으로 Component scan은 해당 패키지와 하위 패키지 내의 모든 클래스를 살펴보고 @Component 어노테이션이 붙어있는 클래스를 빈으로 등록한다.

하지만, base package를 지정해 더 상위 혹은 하위의 패키지만 스캔하도록 변경할 수 있다.

또, 필터를 통해 특정 어노테이션 포함 여부, 정규식, 커스텀 필터 등을 지정해 스캔되도록 설정할 수 있다.

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-scanning-filters

@Configuration
@ComponentScan(basePackages = "org.example",
        includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
        excludeFilters = @Filter(Repository.class))
public class AppConfig {
    // ...
}

생성자 주입

수정자(setter), 메서드, 필드 주입 외에도 생성자를 통해 의존 관계를 주입받을 수 있다.

(ComponentScan을 사용하지 않으면 생성자 주입이 까다롭기 때문에 설명하지 않았었다.)

package com.sample.spring;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.sample.spring.repository.SampleRepository;

@Component
public class Application {

    private final SampleRepository sampleRepository;

    @Autowired
    public Application(SampleRepository sampleRepository) {
        this.sampleRepository = sampleRepository;
    }

    public void run() {
        System.out.println(sampleRepository.getLastUserId());
    }

}

심지어 Spring context 4.3버전 부터는, 생성자가 하나인 경우에 @Autowired 어노테이션 없이 생성자로 의존관계가 주입된다.

package com.sample.spring;

import org.springframework.stereotype.Component;

import com.sample.spring.repository.SampleRepository;

@Component
public class Application {

    private final SampleRepository sampleRepository;

		//@Autowired
    public Application(SampleRepository sampleRepository) {
        this.sampleRepository = sampleRepository;
    }

    public void run() {
        System.out.println(sampleRepository.getLastUserId());
    }

}

@Value

@Value 어노테이션을 통해 빈이 아닌 “값”을 주입할 수 있다.

이렇게 주입된 값은 자료형에 맞추어 자동 변환된다.

public Application(SampleRepository sampleRepository, @Value("Hello, World!") String strTest, @Value("10") int intTest, @Value("true") boolean boolTest, @Value("10.5") double doubleTest) {
    System.out.println(strTest); //"Hello, World!"
    System.out.println(intTest); //10
    System.out.println(boolTest); //true
    System.out.println(doubleTest); //10.5
    this.sampleRepository = sampleRepository;
}

또, @Value 어노테이션은 스프링 표현식(SpEL)을 지원한다.

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions

private final SampleRepository sampleRepository;

public Application(SampleRepository sampleRepository, @Value("#{new String('hello world').toUpperCase()}") String strTest) {
    System.out.println(strTest); //HELLO WORLD
    this.sampleRepository = sampleRepository;
}

@Resource

@Resource 어노테이션(jakarta.annotation.Resource)은 @Autowired와 유사하게 동작한다.

@Autowired가 타입을 통해 빈을 찾는 반면에, @Resource는 이름을 통해 빈을 찾아 주입한다.

생성자 주입 vs 수정자 주입 vs 필드 주입

스프링에서는 공식적으로 생성자 주입을 권장한다.

  • 필드를 final로 설정해 필드에 주입된 의존관계의 불변성을 보장할 수 있다. (수정자 주입은 setter가 public으로 열려있어야 해서 다른 개발자가 setter를 사용해 불변성을 깨트릴 가능성이 있다.)
  • 단위테스트 시 생성자주입이 테스트에 용이하다. (필드 주입은 필드가 private인 경우 값을 변경할 수 없어 테스트 시 수동 주입이 불가능하다.)
반응형

'핥아먹기 시리즈 > Spring Core 핥아먹기' 카테고리의 다른 글

12. Resource 인터페이스  (0) 2022.12.28
11. 기타 기능들  (0) 2022.12.28
9. 의존관계 자동 주입 (@Autowired)  (0) 2022.12.28
8. 빈 생명 주기  (0) 2022.12.28
7. Singleton Pattern  (0) 2022.12.28
profile

while(1) work();

@유호건

❤️댓글은 언제나 힘이 됩니다❤️ 궁금한 점이나 잘못된 내용이 있다면 댓글로 남겨주세요.

검색 태그