컴포넌트 스캔

고동현·2024년 4월 3일
0

Spring 기본

목록 보기
6/10

이번시간에는 컴포넌트 스캔에 대해서 알아보겠다.

우선 그러면, 이걸 왜쓰는걸까? 이것부터 알아보자.

이전에 Appconfig에서 스프링 컨테이너에 객체 인스턴스를 어떻게 등록했는가?
=>대답을 하지 못한다면, 이전글을 다시 읽어보고 오자

@Configuration으로 Appconfig를 등록하고 그안에
@Bean 애노테이션으로 등록을하였다.
아래의 그림과 같이

그러니까 한마디로, 싱글톤 객체로 관리해야할 것을 전부 AppConfig에 등록을 해서 사용했다는 것이다.

그런데, 문제는 프로젝트가 커지고, 이런식으로 AppConfig에 직접 내가 @Bean으로 등록해야할 설정정보들이 많아지면, 귀찮음 + 실수로 누락 시키는경우가 굉장히 많아지게 된다.

그래서 개발자들이 생각한게, 어? 그러면 자동적으로 이렇게 스프링 컨테이너에 등록해주는건 없을까? 싶어서 만든게
컴포넌트 스캔이다.

이제 컴포넌트 스캔에 대해서 알아보자.
AutoAppConfig.java

이번에는 AutoAppConfig가 있는데 @Configuration은 동일한데, @ComponentScan을 붙여주었다.

여기까지만 보고 일단,
왜? 1. @Configuration을 붙이고 2. @ComponentScan을 붙였는지 생각해보자, 그에 대한 대답은 뒤에서 설명해보겠다.

그전에 excludeFilter에 대해서 설명하자면, 우리가 이전에 AppConfig에서도 @Configuration, @Bean으로 등록을 하였다.

그런데 우리는 AppConfig없이 AutoAppConfig로만 하고싶은데
이 AppConfig파일을 지우지 않는이상, 스프링 컨테이너가 올라오면서 자동으로 이 AppConfig에 있는 빈을 등록해버리니까(왜냐하면 Configuration안에 @component가 있으므로 컴포넌트 스캔을 해버리면 Configuration설정정보도 전부 등록됨)
Configuration 애노테이션은 exclude 배제 하겠다는것이다.

그리고 이제 Component 스캔을 해보자.



이렇게 ComponentScan과 AppConfig의 차이점을 알아 보겠는가?

맨위에 사진에서 AppConfig를 사용하면
빈이름: memberService에 value로 MemberServiceImple@x01
빈이름: orderService에 value로 OrderSerivceImple@x02
...
이런식으로 value에 인터페이스에 해당하는 구현체 인스턴스가 value로 들어가게 된다.

그러나 이제는 직접적으로,
빈이름 memberServiceImpl 빈 객체 MemberServiceImple@0x01
빈이름 orderServiceImpl 빈 객체 OrderServiceImple@x02
...
이런식으로 등록이 된다.

그런데 여기까지 오면 궁금한점이 있다.
AppConfig에서 보면 의존관계 주입이 명시가 되어있다.
그러니까,

이런식으로 AppConfig에서는 memberServiceImple구현체에는 memberRepository 인스턴스가 필요해라는 정보인데
이전에는 AppConfig를 Configuration으로 등록을하면, 스프링이 자동적으로 필요한 인스턴스를 컨테이너에서 가져와서 의존관계 주입을 해주었다.

그런데 AutoAppConfig를 생각해보자
아무것도 없다. 비어있으니까 말이다.
그래서 memberServiceImple로 가보면,

이 생성자에 필요한 의존관계 주의 정보가 있다.
MemberServiceImpl에는 memberRepository가 필요하다는 것이다.
그래서 이것을 @Autowired 애노테이션을 통해서 스프링 컨테이너에서 의존관계 주입을 받는다.

그러면 여기서 또 궁금한게 있다. 어? memberRepository는 컴포넌트 스캔을 안했는데 어떻게 컨테이너에서 가져오지? 라는 생각을 할 수 있다.

왜냐하면 컴포넌트 스캔후 현재 스프링 컨테이너가 이상태일테니 말이다.

그런데 스프링 컨테이너는 Type으로 조회한다는것을 기억하고 있는가?
만약 부모타입으로 조회하면 자식타입까지 다튀어 나온다.
그래서 memberRepository를 상속받고있는 memoryMemberRepository 자식 까지 다 뒤져서 해당하는 동일 type의 객체 인스턴스를 자동적으로 의존관계주입을 해준다.
그러면 여기서 또 궁금한게
어? 그러면 만약 memoryMemberRepository와 dbMemberRepository등으로 두개가 있으면 뭘넣지? 라는 궁금증이 생길 수 있다. 이건 뒤에서 설명하겠다.

어쨋든, 여기까지 설명을 해보자면,
직접 빈 애노테이션으로 Configuration,Bean을 이용해 AppConfig에서 사용하기 힘드니,
직접 스프링 컨테이너에 등록하고싶은 class를 @Component 애노테이션으로 등록 후, 필요한 의존관계주입은 @AutoWired를 사용해서 한다는 것을 알았다.

여기까지 설명을 듣고 아까 말한 2가지 질문에 대답을 할 수 있겠는가?
1. @Configuration을 붙이고 2. @ComponentScan을 붙였는지 생각해보자.

1번의 답과 2번답은 결국 그냥이다.
그냥? 이게 무슨말이냐 code에 그냥이 어디있냐 싶을 수 있겠지만 진짜다.
이게 AppConfig과 최대한 비슷하게 하면서 설명을 하기위해서 이런식으로 했다.

자세히 설명을 해보자면, AppConfig에서 Configuration을 붙인 이유는 명확하다. 왜? 결국 AppConfig를 설정정보로 등록해서, Bean 애노테이션으로 APpConfig에서 설정해놓은 Class들을 컨테이너에 올리기 위해서,
그런데 여기 AutoAppConfig에서는 설정정보가 그 무엇도 없다.
그러니까 @Configuration이 없어도 제대로 동작한다.
실제로 지우고 돌려봐라. 제대로 돌아간다.

2번, @ComponentScan 이게 굳이 있어야할 이유가 있을까?

마치 여기서 excludeFilter를써서, AutoAppConfig에서ComponentScan을 붙여야만,
다른 @Component를 붙인 class,
즉. memoryMemberRepository, MemberServiceImpl 인스턴스가 스프링 컨테이너에 올라갈꺼 같지만,(풀어서 말하면 @Component를 붙인 인스턴스를 스프링 컨테이너에 올리기 위해서는 AutoAppConfig에서 @ComponentScan를 붙여줘야한다.)
전혀 아니다.

아에 AutoAppConfig를 삭제하고 돌려보자.
제대로 실행된다. 그이유가 뭘까?

@SpringBootApplication 내에 @ComponentScan이 있다. 그래서, 실무에서는 보통 해당 @SpringBootApplication이 붙은 메인 클래스를 기준으로 해당 패키지와 하위 패키지에 빈으로 사용할 클래스들을 생성하고 @Component를 붙여주기 때문에 @AutoAppConfig가 없어도 되는것이다.

고로 @Component를 class에 붙인것들은 결국 스프링이 올라가면서 그때, Component가 달린것들을 전부 뒤져서 컨테이너에 올려주는것이다.

그러면 SpringBootApplication은 어디서 부터 ComponentScan을 뒤질까?

여기서 보면 package가 hello.core이다.
여기서 뭐 지정한게 없으므로, 이 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
그러면 hello.core 밑에 모든 하위패키지를 다 뒤져서 스프링 컨테이너에 등록해준다.

여기서 바로 중요한게 나온다.
아까 질문 2가지 중에서 여기서 ComponentScan이 있는것을 기억할것이다. package 위치도 hello.core이다.

동일하게 package가 hello.core인건 맞지만 실무에서 이 범위를 변경 할 수도 있을 것이다. 사실 그런경우가 왜 필요한지까지는 모르겠지만, 만약 ComponentScan의 범위가 최상단이 아닌 그 아래 패키지 부터 시작하고 싶을 수 도 있을 것이다.

그렇게 하기 위해서 AutoAppConfig의 @ComponentScan이 달려있는것이다.
[메인클래스의 @ComponentScan의 범위를 제한해줘야 할 때]

그렇게 하기 위해서


여기에서 "hello.core.service"이런식으로 자유롭게 변경이 가능하다.
그러면 이제 ComponentScan범위가 hello.core.service에서 시작하는것이다.

컴포넌트의 스캔 기본대상은 꼭 @Component뿐만아니라
@Component,@Controller,@Service,@Repository,@Configuration등이 있다.
각 애노테이션을 까보면 @Component를 포함하는것을 확인 할 수 있다.

필터

여기서 이제 2가지 질문중 2번이 그냥이 아니게 되는것이다.
우선 두가지 애노테이션을 만들어보자.

  1. 컴포넌트 스캔 대상에 추가할 애노테이션
  2. 컴포넌트 스캔 대상에서 제외할 애노테이션

    컴포넌트 스캔 대상에 추가할 클래스

    컴포넌트 스캔 대상에서 제외할 클래스

TestCode

원래대로라면, @Component가 BeanA,BeanB가 없다. 그러면 스프링 컨테이너에 등록이 안되는게 정상이다.
하지만, MyInClude,MyExClude 애노테이션이 각각 달려있고,
빈으로 등록할 class를 filter로 조절하기 위해서
@ComponentScan을 ComponentFilterAppConfig에 달은 것이다.
그러면 includeFilters = ~내가 빈으로 등록할 class filter
excludeFilters = ~내가 빈으로 등록 제외할 class filter
이런식으로 구분해서 빈으로 등록하게 된다.

이제 2번의 이유가 그냥이 아닌 이유를 알겠는가?
AutoAppConfig에서 만약 이런식으로 애노테이션 filter를 적용하거나, 혹은 ComponentScan을 할 범위를 바꾸는 이런 역할 없이,
그냥 memoryMemberRepository,OrderServiceImple,,등등 이런것들을 스프링 컨테이너에 등록하는건 어차피, SpringBootApplication에 있는 ComponentScan이 해줄것이다.

다만 필터예제와 같이, 애노테이션 filter를 적용하거나, 혹은 ComponentScan을 할 범위를 바꾸는 역할이 필요할때, 따로 AppConfig를 만들어 @ComponentScan애노테이션을 붙인다.

앞선 예제에서는 이러한 기능이 없기때문에, 진짜 그냥 만든것이라고 표현을 했을 뿐이다.

참고
filterType에는 5가지 옵션이 있다.
Annotaion:기본값, 애노테이션을 인식해서 동작
ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
ASPECTJ: AspectJ 패턴 사용
REGGEX: 정규표현식
CUSTON: TypeFilter라는 인터페이스를 구현해서 처리

사실.. 나도 이게 정확히 어떤 기능을 하는지까지 모르겠다..

참고로 includeFilter 이런걸 사용할 일이 거의없다. excludeFilter같은경우 간혹 사용할수도 있겠으나 어차피 생각해보면 @Component를 붙이면 되지 includeFilter를 써야할 이유가 뭐가 있겠는가.

중복등록과 충돌

그렇다면 만약 빈이름이 중복되면 어떻게 될까?
두가지 경우가 있을 것이다.

  1. 자동 빈 등록 vs 자동 빈 등록
  2. 수동 빈 등록 vs 자동 빈 등록

1번경우를 보자
컴포넌트 스캔에 의해서 자동으로 스프링 빈이 생성되는데 이름이 같으면, 그냥 Error를 throw한다.

  • ConflictingBeanDefinitionException 예외 발생

2번의 경우가 좀 다른데
일단 상식적으로 생각해도 자동보다는 구체적인 수동빈이 이길꺼 같은 느낌이 싹 들긴한다.

예를 들어 이런식으로 한는것이다.

@Component
public class MemoryMemberRepository implements MemberRepository{}

이렇게하면, 이건 스프링 올라오면서 스프링 컨테이너로 컴포넌트 스캔을 전부 해서 올려준다.

그런데, 이제 AutoAppConfig에도 넣어두는것이다.

@Configuration
@ComponentScan
public class AutoAppConfig{
   @Bean(name = "memoryMemberRepository)
   public MemberRepository memberRepository(){
      return new MemoryMemberRepository();
   }
}

여기서 name이 왜 memoryMemberRepsitory인지는 알겠는가?
바로, Component애노테이션이 달려있으면 이 class명의 맨앞이 소문자로 바뀌어서 들어간다고 아마 지난시간에 설명을 했을 것이다.
그래서 이렇게 @ComponentScan을 하면 이름이 중복이 된다.

그런데 이런경우 수동 빈 등록이 우선권을 가진다.
그러므로, 수동빈이 자동빈을 오버라이드 하는것이다.

수동빈 등록시 남는 로그

최근 스프링 부트에서는 자동 빈 등록과 수동 빈 등록이 충돌이 나면, 오류가 발생하도록 바꾸었다.

참고).
내가 조금 헷갈렸던 점이, 어? 그러면 2번과 같은 경우에서
먼저 @SpringBootApplication에 있는 컴포넌트 스캔이 먼저 작동해서, memoryMemberRepository객체가 먼저 등록이 되고, 그다음에 AppConfig에 있는 컴포넌트 스캔이 그 다음에 동작하는 이런 순서가 있는건가?
싶었는데

뭐 순서가 있는것 까지는 아니고 어떻게 이해를 해야하냐면,

기본적으로 @SpringBootApplication에 의한 스캔이 시작점이 되며, 여기에 포함되지 않는 추가적인 컴포넌트 스캔이 필요한 경우 별도의 설정 클래스에서 @ComponentScan을 사용하여 지정할 수 있다.

이는 모두 Spring 애플리케이션 컨텍스트가 초기화되는 동안 함께 처리된다. 따라서, 특정한 "순서"라기보다는 애플리케이션 컨텍스트를 구성하는 전체적인 과정의 일부로 이해하는 것이 좋다.

-The End-

profile
항상 Why?[왜썻는지] What?[이를 통해 무엇을 얻었는지 생각하겠습니다.]

0개의 댓글

관련 채용 정보