[스프링 핵심 원리] 컴포넌트 스캔

JUJU·2024년 2월 19일
0

Spring

목록 보기
6/21
본 포스트는 김영한 개발자님의 스프링 핵심 원리 강의를 듣고 정리한 것입니다.
※ 코드는 강의에서 사용된 것과 다릅니다.
jaewon-ju Github Address

새로운 문제
스프링 컨테이너에 빈을 등록하기 위해서는 @Bean 어노테이션을 사용했다.
하지만, 프로젝트가 커지면 AppConfig 클래스에 일일이 빈 코드를 작성하고 @Bean 어노테이션을 반복해서 작성해야한다.


해결 방법
스프링은 설정 정보가 비어있어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔 기능을 제공한다.



✏️ 컴포넌트 스캔

컴포넌트 스캔은 @Component 어노테이션이 붙은 클래스를 빈으로 자동 등록하는 기능이다.

  • 빈의 이름은 클래스의 제일 앞 대문자를 소문자로 바꿔서 저장한다.
    ex) MemberRepository 클래스 -> memberRepository 빈

수동

@Configuration
public class AppConfig {

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public BoardPolicy boardPolicy(){ 
    	return new ReadOnlyPolicy(); 
    }
    @Bean
    public BoardService boardService(){
        return new BoardServiceImpl(memberRepository(), boardPolicy());
    }
}
⬇︎ 자동(컴포넌트 스캔)
  • 설정 정보 클래스를 비움
  • 객체(빈)로 등록할 클래스에 @Component 어노테이션을 붙임
@Configuration
public class AutoAppConfig {
	// 비어있다
}
@Component
public class MemoryMemberRepository implements MemberRepository{ ... }
@Component
public class MemberServiceImpl implements MemberService { ... }
@Component
public class BoardServiceImpl implements BoardService{ ... }
@Component
public class ReadOnlyPolicy implements BoardPolicy { ... }

⚠️ 다수의 @Configuration

하나의 프로그램에 여러개 @Configuration 어노테이션이 존재하는 경우

@Configuration 어노테이션은 @Component 어노테이션을 포함하고 있다.
➜ @Configuration이 붙은 클래스도 빈으로 등록된다.
➜ @Configuration 어노테이션이 여러개 존재하면 충돌이 발생할 수 있다.
∴ 필터를 사용해서 스캔 대상을 선택한다.

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

⚠️ @Autowired

@Component 어노테이션이 붙은 클래스는 빈으로 자동 등록된다.
하지만, 스프링의 핵심 기술인 의존관계 주입은 추가로 설정 해줘야한다.

다음의 컴포넌트를 한번 보자.

@Component
public class BoardServiceImpl implements BoardService{

    private final MemberRepository memberRepository;
    private final BoardPolicy boardPolicy;

    public BoardServiceImpl(MemberRepository memberRepository, BoardPolicy boardPolicy) {
        this.memberRepository = memberRepository;
        this.boardPolicy = boardPolicy;
    }

(MemberRepository memberRepository, BoardPolicy boardPolicy)
해당 파라미터를 전달해줘야 스프링 컨테이너의 핵심 원리인 의존관계 주입 기능이 작동할 것이다.

이 때 생성자에 @Autowired 어노테이션을 붙여주면 의존관계 주입이 자동으로 처리된다.
@Autowired 어노테이션은 컨테이너에서 타입이 같은 빈을 찾아서 매개 변수에 전달한다.
ac.getBean(타입)과 같은 메커니즘으로 동작한다.

    @Autowired
    public BoardServiceImpl(MemberRepository memberRepository, BoardPolicy boardPolicy) {
        this.memberRepository = memberRepository;
        this.boardPolicy = boardPolicy;
    }



✏️ 탐색 위치와 스캔 대상

■ 탐색 위치 지정

basePackages 옵션을 사용하여 탐색 시작 위치를 지정할 수 있다.

@ComponentScan(
         basePackages = "boardProject.demo"
         // boardProject.demo 패키지에서 탐색 시작
}

탐색 위치를 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지부터 탐색이 시작된다.

탐색 위치를 지정하는 것보단, 설정 정보 클래스를 패키지 최상단에 두는 방법을 사용하자.


■ 컴포넌트 스캔 대상

컴포넌트 스캔은 @Component뿐만 아니라 다음 어노테이션도 스캔 대상으로 본다.

Annotation기능
@Configuration설정 정보로 인식
@ControllerMVC Controller로 인식
@Repository데이터 접근 계층으로 인식, 데이터 계층의 예외를 추상화 시켜줌
@Service기능은 없음. @Service가 붙은 빈은 핵심 비즈니스 로직을 갖고 있는 컴포넌트임을 알리는데 의의를 둠



✏️ 중복 등록

등록하려는 빈의 이름이 같은 경우, 충돌이 발생한다.

■ 수동 빈 VS 자동 빈

수동으로 등록된 빈과 자동으로 등록된 빈이 충돌하면 수동 빈이 우선권을 가진다.

// 수동 빈
@Configuration
public class AutoAppConfig {
     @Bean(name = "memoryMemberRepository")
     public MemberRepository memberRepository() {
         return new MemoryMemberRepository();
     }
}
// 자동 빈
@Component
public class MemoryMemberRepository implements MemberRepository { ... }

위의 예시처럼 이름이 같은 수동 빈과 자동 빈이 충돌하면, 수동 빈이 자동 빈을 오버라이딩 한다.

하지만, 최근 스프링 부트에서는 수동 빈과 자동 빈이 충돌하면 오류를 발생하도록 기본 값이 바뀌었다.


■ 자동 빈 VS 자동 빈

등록하려는 빈의 이름이 같은 경우, ConflictingBeanDefinitionException이 발생한다.




✏️ 중복 주입

주입하려는 빈의 타입이 같은 경우, 충돌이 발생한다.
BoardServiceImpl 클래스를 살펴보자.

@Component
public class BoardServiceImpl implements BoardService{

    private final MemberRepository memberRepository;
    private final BoardPolicy boardPolicy;

    @Autowired 
    public BoardServiceImpl(MemberRepository memberRepository, BoardPolicy boardPolicy) {
        this.memberRepository = memberRepository;
        // MemoryMemberRepository 클래스만 @Component가 붙어있음
        this.boardPolicy = boardPolicy;
        // ReadOnlyPolicy, ReadWritePolicy 클래스 모두 @Component가 붙어있음
    }

@Autowired는 생성자를 호출할 때 각 타입에 맞는 빈을 찾아서 의존관계를 주입한다.
ac.getBean(BoardPolicy.class)로 매개변수 타입에 맞는 빈을 찾을 때 문제가 발생한다.

현재, ReadOnlyPolicy 클래스와 ReadWritePolicy 클래스가 모두 Component로 등록되어 있다.
두 클래스 모두 BoardPolicy 인터페이스의 구현체이므로, ac.getBean(BoardPolicy.class)에 잡힌다.

➜ NoUniqueBeanDefinitionException 발생


해결 방법
1. @Primary 사용
2. @Autowired 필드/파라미터 명 매칭
3. @Qualifier 사용

1. @Primary

@Autowired가 작동할 때 여러빈이 매칭되면 @Primary가 붙은 빈이 우선권을 가진다.

@Component
@Primary
public class ReadOnlyPolicy implements BoardPolicy { ... }

ReadOnlyPolicy 클래스가 ReadWritePolicy 보다 우선권을 가진다.


2. @Autowired 필드/파라미터 명 매칭

@Autowired는 생성자를 호출할 때 각 타입에 맞는 빈을 찾아서 의존관계를 주입한다.
이때, 중복되는 빈이 발견되면 파라미터 명을 추가적으로 비교한다.

다음 강의에서 배울 @Autowired 필드에도 적용된다.


3. @Qualifier

@Qualifier는 추가 구분자를 붙여주는 방법이다.
⚠️ 이름을 변경하는 것은 아니다!

@Component
@Qualifier("mainPolicy")
public class ReadOnlyPolicy implements BoardPolicy { ... }
@Component
@Qualifier("subPolicy")
public class ReadWritePolicy implements BoardPolicy { ... }

생성자 파라미터에도 @Qualifier를 붙여줘야 한다.

@Autowired 
    public BoardServiceImpl(MemberRepository memberRepository, 
    		@Qualifier("mainPolicy") BoardPolicy boardPolicy) {
        this.memberRepository = memberRepository;
        this.boardPolicy = boardPolicy;
    }

@Primary와 @Qualifier 중 뭐가 더 나을까?

95%의 상황에서 ReadOnlyPolicy 클래스를 사용하고 나머지 상황에서 ReadWritePolicy를 사용한다고 가정해보자. 이 경우에는 ReadOnlyPolicy에 @Primary를 붙이고, ReadWritePolicy에 @Qualifier를 붙여서 명시적으로 호출하는 것이 코드를 깔끔하게 유지할 수 있다.

실제로 중복된 두 빈중에 선택하고 싶을 때는 어떻게 할까?

클라이언트가 주입할 빈을 선택하도록 만들고 싶다면, 중복된 빈을 모두 가져와서 선택된 빈을 주입해야 한다. 이 경우에는 Map을 사용해서 해당 타입의 빈을 모두 주입받을 수 있다.
다음 포스팅에서 자세히 설명하겠다.




REFERENCE

스프링 핵심 원리 - 김영한 개발자님

profile
개발자 지망생

0개의 댓글

관련 채용 정보