기록/Spring framework

@Component, 컴포넌트 스캔( 자동 스프링 빈 등록). 2021-11-23

최동훈1 2021. 11. 23. 16:20

우리는 그전까지는 @Configuration 애너테이션을 통해서 직접 스프링 설정 클래스를 작성해서 스프링 컨테이너에 등록하였다. 이 방법은 설정 클래스의 코드가 너무 길어진다는 단점이 있다.

앞 포스팅에서 설명했듯, @Autowired도 기존에는 생성자나 세터메서드 방식으로 의존 주입하던 방식에서 스프링에서 자동으로 의존 주입을 해준다. 그렇기때문에 설정 클래스의 코드가 더 간결해졌다.

이 @Component 애너테이션도 마찬가지이다. 이 애너테이션을 스프링 빈 객체로 등록하고자 하는 클래스에 붙이면, 스프링 설정클래스(@Configuration) 에서 등록할 필요 없이 스프링이 알아서 등록 해 준다. 

예를 들면 기존에는,

@Configuration
public class AppCtx {

	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}
	
	@Bean
	public MemberRegisterService memberRegSvc() {
		return new MemberRegisterService();
	}
	
	@Bean
	public ChangePasswordService changePwdSvc() {
		return new ChangePasswordService();
	}
	
	@Bean
	@Qualifier("printer")
	public MemberPrinter memberPrinter1() {
		return new MemberPrinter();
	}
	
	@Bean
	@Qualifier("summaryPrinter")
	public MemberSummaryPrinter memberPrinter2() {
		return new MemberSummaryPrinter();
	}
	
	@Bean
	public MemberListPrinter listPrinter() {
		return new MemberListPrinter();
	}
	
	@Bean
	public MemberInfoPrinter infoPrinter() {
		MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
		return infoPrinter;
	}
	
	@Bean
	public VersionPrinter versionPrinter() {
		VersionPrinter versionPrinter = new VersionPrinter();
		versionPrinter.setMajorVersion(5);
		versionPrinter.setMinorVersion(0);
		return versionPrinter;
	}
}

이렇게 애너테이션을 통해 설정 클래스가 구성되었다. 위에 @Qualifier를 붙인 이유는 두 객체 모두 MemberPrinter 타입을 상속받기 때문에 만약 해당 객체의 타입을 @Autowired를 통해 의존주입해줘야하는 입장이면, @Qualifier한정자를 통해 구별해 줘야 한다. 왜냐하면 같은 타입의 빈 객체가 2개이기 때문이다.

 

다시 본론으로 돌아와서 이렇게 복잡한 스프링 설정 클래스를 빈객체에 등록하고 싶은 클래스 위에 @Component 를 붙임으로써 간결한 코드로 만들 수 있다.

@Configuration
@ComponentScan(basePackages = {"spring"})
public class AppCtx {

	@Bean
	@Qualifier("printer")
	public MemberPrinter memberPrinter1() {
		return new MemberPrinter();
	}
	
	@Bean
	@Qualifier("summaryPrinter")
	public MemberSummaryPrinter memberPrinter2() {
		return new MemberSummaryPrinter();
	}
	
	@Bean
	public VersionPrinter versionPrinter() {
		VersionPrinter versionPrinter = new VersionPrinter();
		versionPrinter.setMajorVersion(5);
		versionPrinter.setMinorVersion(0);
		return versionPrinter;
	}
}

그리고 스프링 설정 클래스에서 @ComponentScan 애너테이션을 사용하면 아래와 같이 @Component 애너테이션이 붙은 클래스들을 자동으로 스프링 빈으로 등록한다.

@Component
public class MemberDao {

	private static long nextId = 0;

	private Map<String, Member> map = new HashMap<>();

	public Member selectByEmail(String email) {
		return map.get(email);
	}

	public void insert(Member member) {
		member.setId(++nextId);
		map.put(member.getEmail(), member);
	}

	public void update(Member member) {
		map.put(member.getEmail(), member);
	}

	public Collection<Member> selectAll() {
		return map.values();
	}
}

위의 클래스처럼 @Compoent 애너테이션에 아무런 값(식별자)를 주지 않으면, 디폴트 값으로는 해당 클래스 이름의 앞글자를 소문자로 한 "memberDao"라는 이름으로 빈에 등록된다. 만약 빈 이름을 명시적으로 지정하고 싶다면 아래와 같이,  @Component("infoPrinter") 이렇게 문자열 값을 애너테이션 뒤에 넣어주면 된다.

@Component("infoPrinter")
public class MemberInfoPrinter {

	private MemberDao memDao;
	private MemberPrinter printer;

	public void printMemberInfo(String email) {
		Member member = memDao.selectByEmail(email);
		if (member == null) {
			System.out.println("데이터 없음\n");
			return;
		}
		printer.print(member);
		System.out.println();
	}

	@Autowired
	public void setMemberDao(MemberDao memberDao) {
		this.memDao = memberDao;
	}

	@Autowired
	@Qualifier("printer")
	public void setPrinter(MemberPrinter printer) {
		this.printer = printer;
	}

}

또 스프링 설정 클래스에서 컴포넌트 스캔을 할때, 스캔 범위를 지정해 주어야 하는데, @ComponentScan( basepackages= {"패키지명"}) 이렇게 값을 주면 해당 패키지명 하위에 있는 모든 패키지와 클래스 파일들을 스캔한다.

 

만약 이름이 똑같은 스프링 빈이 하나는 @Configuration 설정클래스에서 수동등록, 또 다른 하나는 @Component를 통한 자동등록을 했을때, 수동으로 등록한 빈이 우선적으로 등록되서 결국 스프링 빈으로는 최종적으로 1개의 빈만 등록된다.

 

하지만 컴포넌트 스캔을 통해 각각 다른 패키지의 같은 이름의 빈을 자동등록하면 문제가 생긴다. 두 객체가 같은 이름으로 등록된다는 익셉션이 나오는데, 이때는 @Component("식별자") 이렇게 빈 이름을 다르게 함으로써 해결 할 수 있다.

 

*요약 하자면,

같은 빈 이름 으로 2개의 객체를 자동 등록-> 애러남.

다른 빈 이름으로 2개의 같은 타입의 객체를 수동등록 -> 의존주입할때 @Qualifier로 구별.