SPRING

컴포넌트 스캔, 의존관계 자동 주입

설정 정보에 빈들을 등록하는 것이 한눈에 알아보기는 쉽지만 만약 이러한 빈이 수십수백 개가 되어버린다면 실수하기 쉽거나 감당하기가 어렵습니다. 그래서 스프링에서는 설정 정보가 따로 없어도 자동으로 스프링 빈을 등록해주는 컴포넌트 스캔이라는 기능을 제공합니다. (추가적으로 의존관계 역시 자동으로 주입해주는 @Autowired 기능도 있습니다.)

이번장에서는 컴포넌트 스캔의존관계 자동 주입에 대해 알아봅시다.

@Component

사전준비

우선 컴포넌트 스캔을 사용하기 위해서 @ComponentSacn을 설정 정보에 붙여주어야 합니다.

@Configuration
@ComponentScan(excludeFilters = @Filter(type = FilterType.ANNOTATION, 
		classes = Configuration.class))
public class AutoAppConfig {
 
}

주목할 점

  • @Bean을 통해 등록한 빈이 하나도 없다
  • @Configuration이 붙은 설정 정보들도 자동으로 등록되기 때문에 다른 설정 정보를 담은 Configuration.class를 제외
  • 기타 설정은 추가 설정 참고

 

사용

컴포넌트 스캔하는 말 그대로 @Component이 붙은 클래스를 스캔해 스프링 빈으로 등록합니다.

빈으로 등록할 모든 클래스에 @Component 어노테이션을 붙여주면 됩니다.

@Component
public class MemberRepositoryByDB implements MemberRepository {
	...
}

추가적으로 빈의 이름은 디폴트 값으로 입력됩니다. (클래스 명과 동일) 빈의 이름을 따로 설정해주고 싶으면 @Component("빈 이름") 형식으로 입력하면 됩니다.

 

어노테이션 설명
@Component 클래스를 스캔해 빈으로 등록하게 하는 어노테이션
@Controller 스프링 MVC 컨트롤러로 인식 (@Component의 전문화)
@Repository 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환
(@Component의 전문화)
@Service 스프링에서는 다른 처리를 하지 않으나, 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나 라고 비즈니스 계층을 인식하는데 도움을 줌(@Component의 전문화)
@Configuration 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가처리
(@Component의 전문화)

 

 

중복 등록과 충돌

빈 이름이 같은 경우 충돌이 발생할 수 있습니다. 이런 경우 ConflictingBeanDefinitionException 예외가 발생합니다.

 

 

추가 설정

기능 설정방법
빈의 이름 설정 @Component("빈 이름")으로 설정 가능
탐색 시작 위치 지정 @ComponentSace의 basePackages 속성을 사용. 시작위치 중복 설정이 가능하다.
(보통은 설정정보를 프로젝트 최상단에 두고, 이 기능을 사용하지 않는다)
필터 반드시 포함 : includeFilters (Component를 사용하면 되서 잘 쓰지 않는다.)
반드시 미포함 : excludeFilters 방식은 위 참고

필터 타입 옵션

종류 설명 예시
annotation (default) 기본값으로 어노테이션을 인식해 동작. org.example.SomeAnnotation
assignable 지정한 타입과 자식 타입을 인식해 동작 org.example.SomeClass
aspectj AspectJ 패턴 사용 org.example..*Service+
regex 정규 표현식 org\.example\.Default.*
custom TypeFilter라는 인터페이스를 구현해 처리 org.example.MyTypeFilter

 

 

자바 코드

@Configuration
@ComponentScan(basePackages = "hello.spring.repository", "hello.spring.service",
        includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
        excludeFilters = @Filter(Repository.class))
public class AutoAppConfig {
 
}

 

XML

<beans>
    <context:component-scan base-package="org.example">
        <context:include-filter type="regex"
                expression=".*Stub.*Repository"/>
        <context:exclude-filter type="annotation"
                expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>
</beans>

 

@AutoWired

각 의존관계 주입 방법을 짚고 넘어가 봅시다.

 

생성자 주입

생성자를 통해서 의존관계를 주입받는 방법입니다.

  • 생성자의 특징대로 생성시점 딱 한 번만 호출되는 것이 보장됩니다.
  • set의 사용을 막아, 만약의 호출에 대한 제약을 걸 수 있습니다.
  • 불변, 필수 의존관계에 사용
    • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없습니다.
    • 오히려 변경되어서는 안 됩니다!

추가적으로 스프링 빈일 경우 생성자가 그 빈에서 하나만 있을 때 자동으로 @AutoWired가 등록됩니다.

public class MemberService {
	private final MemberRepository memberRepository;
    
    // 생성자가 하나만 있는 경우 @Autowired 써도되고 안써도 된다.
    public MemberSerbice(MemberRepository memberRepository) {
    	this.memberRepository = memberRepository;
    }
}

 

 

수정자 주입

setter라는 수정자 메서드를 통해서 의존관계를 주입하는 방법입니다.

파라미터로 전달된 값을 내부의 인스턴스 변수에 저장하기 때문에 입력 값에 대한 검증이나 그 밖의 작업을 수행할 수 있습니다.

주로 선택 및 변경 가능성이 있는 의존관계에 사용합니다.

  • 선택 : 필수 값이 아닌 경우 @AutoWired(required = false)를 추 기해 의존관계를 선택적으로 주입할 수 있습니다.
  • 변경 : 도중에 인스턴스를 변경하고 싶은 경우 외부에서 수정자를 강제로 호출하면 됩니다.

단점

  • 수정자 주입을 사용하면 setXXX 메서드를 public로 열어두어야 합니다. -> 실수로 호출할 가능성이 있다.
  • 위의 문제로 인해 불변성을 보장할 수 없습니다.

 

 

일반 메서드 주입

수정자 주입과 같은 형태이지만 한 번에 여러 개의 파라미터를 갖고 싶은 경우에 사용됩니다.

보통 생성자와 수정자 선에서 끝나고 일반 메서드 주입은 잘 사용하지 않습니다. (차라리 생성자를 씀)

 

 

 

튜닝

@Autowired는 타입으로 조회를 하는 방식입니다. getBean(Type)과 똑같은 동작 방식이죠.

그리고 이전 포스트에서 타입을 통한 조회 방식은 같은 타입이 둘 이상 있을 때 문제점이 생긴다는 것을 배웠습니다.

이런 경우 어떻게 해야 할까요?

 

@Autowired

여러 빈을 탐색한 경우 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭을 시도합니다.

// MemberRepositoryByMemory implement MemberRepository
// MemberRepositoryByDB implement MemberRepository
// 같이 같은 타입을 가지는 두 빈이 있는 경우...


// 다음과 같은 경우 에러 발생
@Autowired
private MemberRepository memberRepository


// 타입이 같은 경우 변수 이름을 통해 유추
@Autiwired
private MemberRepository memberRepositoryByDB	// MemberRepositoryByDB 호출

 

 

@Qualifier

빈 이름을 변경하는 대신 각 빈에 추가적인 구분 방법을 부여하는 방식입니다.

@Autowired 방식에서 설명한 이름으로 추가 매칭을 시도하는 것과 같다고 보시면 됩니다.

@Primary 보다 선택 프로세스를 더 많이 제어해야 하는 경우(선택지가 많은 경우) Spring의 @Qualifier를 사용합니다.

// 빈 직접 등록 + Qualifier 등록
@Bean
@Qualifier("mainMemberRepository")
public class MemberRepository memberRepository() {
	return new MemberRepositoryByDB;
}

// 컴포넌트 스캔 + Qualifier 등록
@Repository
@Qualifier("MemberRepositoryForTest")
public class MemberRepositoryByMemory implements MemberRepository {}

@Repository
@Qualifier("mainMemberRepository")
public class MemberRepositoryByDB implements MemberRepository {}


// 자동 주입 예시 (생성자ver)
@Autowired
public MemberService(@Qualifier("mainMemberRepository") MemberRepository memberRepository) {
	this.memberRepository = memberRepository;
}

// 자동 주입 예시 (수정자ver)
@Autowired
public MemberRepository setMemberRepository(@Qualifier("mainMemberRepository") 
		MemberRepository memberRepository) {
	return memberRepository;
}

그래도 찾을 수 없는 경우 NoSuchBeanDefinitionException 예외를 반환합니다.

 

 

@Primary

@Primary는 우선순위를 정하는 방법입니다. 여러 빈이 검색되는 경우 @Primary가 붙어있는 빈이 우선권을 가집니다.

// 컴포넌트 스캔 + Qualifier 등록
@Repository
@Qualifier("MemberRepositoryForTest")
public class MemberRepositoryByMemory implements MemberRepository {}

@Repository
@Primary
public class MemberRepositoryByDB implements MemberRepository {}


// 따로 입력하지 않아도 Primary를 자동으로 찾아 간다.

// 자동 주입 예시 (생성자ver)
@Autowired
public MemberService(MemberRepository memberRepository) {
	this.memberRepository = memberRepository;
}

// 자동 주입 예시 (수정자ver)
@Autowired
public MemberRepository setMemberRepository(MemberRepository memberRepository) {
	return memberRepository;
}

추가적으로 @Qualifier vs @Primary 경우에 @Qualifier 가 우선권을 가집니다.

 

 

어노테이션 직접 만들기

위 방식들을 사용하는 경우 컴파일 시 타입 체크가 안된다는 단점이 있습니다. 이를 극복하기 위해서 어노테이션을 직접 만드는 방법이 있습니다. 

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
	ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainMemberRepository")
public @interface MainMemberRepository { }


// 빈 추가
@Component
@MainMemberRepository
public class MemberRepositoryByDB implements MemberRepository {}


// 자동 주입 예시 (생성자ver)
@Autowired
public MemberService(@MainMemberRepository MemberRepository memberRepository) {
	this.memberRepository = memberRepository;
}

// 자동 주입 예시 (수정자ver)
@Autowired
public MemberRepository setMemberRepository(@MainMemberRepository MemberRepository memberRepository) {
	return memberRepository;
}

 

 

정말로 여러 빈이 모두 필요할 때

예외적으로 중복되는 타입의 빈이 모두 필요한 경우도 있습니다. 이런경우에는 Map을 이용하시면 됩니다.

키 유형이 문자열에 한해 맵 인스턴스도 @Autowired가 가능합니다!

public class MovieRecommender {

    private Map<String, MovieCatalog> movieCatalogs;

    @Autowired
    public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }

    // ...
}

이를 이용해 빈의 종류에 따라 연산을 달리 할 수 있습니다.(전략 패턴)

 

예시

public class AllBeanTest {

	@Test
    void 빈의이름에따라_연산방식_변경() {
    	ApplicationContext ac = new
			AnnotationConfigApplicationContext(AutoAppConfig.class, MemberService.class);
 		MemberService memberService = ac.getBean(MemberService.class);
        
 		Member member = new Member(1L, "테스트1", "비밀번호1");
        
       	 	// 결과 : 테스트1을 DB에 저장했습니다.
 		memberService.connect(member, "memberRepositoryByDB");
		
    }
	static class MemberService {
		private final Map<String, MemberRepository> policyMap;
		private final List<MemberRepository> policies;
        
		public MemberService(Map<String, MemberRepository> policyMap, 
        		List<MemberRepository> policies) {
			this.policyMap = policyMap;
			this.policies = policies;
            
			System.out.println("policyMap = " + policyMap);
			System.out.println("policies = " + policies);
		}
        
        		// 코드에 따라 저장방식이 달라지도록 오버로드
		public void connect(Member member, String connectionCode) {
			MemberRepository memberRepository = policyMap.get(connectionCode);
            
			System.out.println("connectionCode = " + connectionCode);
			System.out.println("memberRepository = " + memberRepository);
            
           		 // 검색된 bean의 connect 메서드 사용
			return memberRepository.connect(member);
		}
	}
}

Map의 get( )을 이용해서 전략패턴을 구현한 모습입니다.

더 자세한 정보는 다음을 참고하시길 바랍니다.

 

 

참고자료

Spring Reference : Core Technologies (spring.io)

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

도서 : 토비의 스프링

강의 : 스프링 핵심 원리 <기본 편> (김영한)

'SPRING' 카테고리의 다른 글

IoC(2) - 빈(Bean)  (0) 2021.10.07
IoC(1) - IoC 개념 및 컨테이너  (0) 2021.10.05
스프링의 등장  (0) 2021.10.04