Programming/Spring framework

빈 객체 등록의 자동화: 컴포넌트 스캔( @Autowired 과는 별개의 개념임. 혼동X) 2021-06-02

최동훈1 2021. 6. 2. 20:14

우선적으로 내가 헷갈린 부분은 컴포넌트 스캔이 마치 @Autowired의 대체재같은 느낌을 받은 것이다. 무언가 자동화...주입..스프링 이런 말을 보고 내가 오해한 것 같다.

하지만 확실하게 구별해야 한다.

@Component : 자바 빈 객체를 @Configuration에서 @Bean 이렇게 설정메서드를 통한 수동 등록이 아닌, 스프링이 편하게 @ComponentScan 애너테이션을 통해 자바 빈에 등록해줄 클래스를 지정하는 애너테이션.

 

@Autowired: 이미 등록된 자바 빈 객체를 의존주입을 위해 기존에는 세터나, 생성자 주입방식으로 DI를 했다면, 이젠 스프링에서 @Autowired가 붙여진 클래스와 같은 타입의 빈 객체를 찾아서 DI 해주기 위해 필드나, 메서드 앞에 붙이는 애너테이션.

 

요약하자면, @Component 는 '빈 객체' 자동 등록을 위한 애너테이션이고, @Autowired는 의존 클래스를 이미 "등록된" 빈 객체에서 찾아, "자동 의존주입"을 위한 애너테이션이다.

 

만약, @Autowired가 붙은 필드나 메서드를 스프링이 실행할때, 해당 타입(의존주입해주려는 클래스)의 빈 객체가 존재하지 않으면 어떻게 될까?

->이 질문에 대한 해답은 간단하다. 오류난다. 이전포스트에도 설명했다.

그럼 반대로 질문이 떠오른다. 만약 의존의 대상이 되는 필드나, 메서드에 null 값이 들어갈때, 특정한 기능을 하게끔 프로그램을 설계했다면, 즉, 일부로 null이 되는 상황(빈객체에 등록된 타입이 없을때)에서도 프로그램이 그에 맞게 동작하도록 하려면 어떻게 해야할까?

말로는 이해가 잘 안될테니 코드로서 설명해보자.

public class MemberPrinter {
	private DateTimeFormatter dateTimeFormatter;
	
	public MemberPrinter() {
		dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");
	}
	
	public void print(Member member) {
		if (dateTimeFormatter == null) {
			System.out.printf(
					"회원 정보: 아이디=%d, 이메일=%s, 이름=%s, 등록일=%tF\n", 
					member.getId(), member.getEmail(),
					member.getName(), member.getRegisterDateTime());
		} else {
			System.out.printf(
					"회원 정보: 아이디=%d, 이메일=%s, 이름=%s, 등록일=%s\n", 
					member.getId(), member.getEmail(),
					member.getName(), 
					dateTimeFormatter.format(member.getRegisterDateTime()));
		}
	}
	
	@Autowired
	public void setDateFormatter(DateTimeFormatter dateTimeFormatter) {
		this.dateTimeFormatter = dateTimeFormatter;
	}

이런 상황이면 어떨까? 여기선 피의존대상이 null일때, (DateTimeFormatter 타입의 빈 객체가 등록되어있지 않을때) 회원정보, 아이디.. 등을 출력하는 방식이 달라진다. 즉, 기존 @Autowired만 쓰면, DateTimeFormatter의 타입에 맞는 빈 객체가 존재하지 않는다고 익셉션이 나올 것이다. 하지만 우리의 의도대로라면, 빈 객체가 등록되어있지 않는다면, null을 세터메서드의 파라미터로 전달되길 원한다.

이럴경우 3가지 방법이 존재한다.

첫번째는, @Autowired의 required 필드를 false로 바꾸는 것이다. 그럼 애초에 스프링은 해당 타입의 빈 객체를 찾을수 없어서 의존주입이 불가하면, 해당 set 메서드를 호출조차 하지 않는다. 그럼, 자연스럽게 dateTimeFormatter은 null이 된다.

두번째는, @Nullable 애너테이션을 사용하는 것이다. 그럼 해당 타입의 빈 객체를 찾을 수 없을때, 스프링은 null 값을 전달한다. 이 경우엔 첫번째 방법과 다르게 set메서드를 호출한다. ->호출하면서 파라미터에 null을 전달해 주는 것이다.

 

세번째는,Optional 타입을 이용하는 것이다. 스프링은 @Autowired가 붙여진 피의존대상의 타입이 Optional인 경우, 해당 타입의 빈 객체를 찾을수 없는 경우, 값이 없는 Optional을 인자로 전달하고, 익셉션을 발생시키지 않는다.

 

3가지 경우를 모두 적용해서 null을 파라미터로 전달가능하게 만들어 본 코드는 이렇게 된다.

public class MemberPrinter {
	private DateTimeFormatter dateTimeFormatter;
	
	public MemberPrinter() {
		dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");
	}
	
	public void print(Member member) {
		if (dateTimeFormatter == null) {
			System.out.printf(
					"회원 정보: 아이디=%d, 이메일=%s, 이름=%s, 등록일=%tF\n", 
					member.getId(), member.getEmail(),
					member.getName(), member.getRegisterDateTime());
		} else {
			System.out.printf(
					"회원 정보: 아이디=%d, 이메일=%s, 이름=%s, 등록일=%s\n", 
					member.getId(), member.getEmail(),
					member.getName(), 
					dateTimeFormatter.format(member.getRegisterDateTime()));
		}
	}
	
	@Autowired(required = false)//1번째 방법:required 사용
	public void setDateFormatter(DateTimeFormatter dateTimeFormatter) {
		this.dateTimeFormatter = dateTimeFormatter;
	}

//	@Autowired//3번째 방법: Optional 사용
//	public void setDateFormatter(Optional<DateTimeFormatter> formatterOpt) {
//		if (formatterOpt.isPresent()) {
//			this.dateTimeFormatter = formatterOpt.get();
//		} else {
//			this.dateTimeFormatter = null;
//		}
//	}
	
//	@Autowired//2번째 방법 : @Nullable 사용
//	public void setDateFormatter(@Nullable DateTimeFormatter dateTimeFormatter) {
//		this.dateTimeFormatter = dateTimeFormatter;
//	}
	
}

 

 

추가 궁금증 ++

만약 @Autowired가 붙어져서 자동 의존주입이 되는 대상인데도, 또 @Configuration 설정클래스에서 수동으로 의존주입하면 어떻게 될까?

->@Autowired가 붙은 피의존대상타입이 더 우선적으로 DI된다.

 

@Component스캔의 전반적인 내용이랑 목적은 이번 포스트 시작할때, 작성을 했다.

세부사항으로 설명할 것은 

@Component 애너테이션을 붙여서 자동 빈 등록할 클래스로 만들때, 빈 이름으로 설정할 값을 같이 넣어주면, 스프링은 컨테이너에 등록할때, 그 빈 이름으로 등록한다.(식별자가 되는것이다.) 만약 값을 주지 않는다면 디폴트로는 해당 클래스의 이름의 첫글자를 소문자로 바꾼 식별자로 등록한다.

@Component("listPrinter")
public class MemberListPrinter {

	private MemberDao memberDao;
	private MemberPrinter printer;

이런식으로 빈 객체 식별자를 지정할 수 있다. 만약 "listPrinter"값을 주지 않았다면, 디폴트로 memberListPrinter란 식별자로 자바빈에 등록한다.

또한 이런 @Component 가 붙은 클래스들을 등록하려면 @ComponentScan 이라는 애너테이션을 @Configuration 클래스 상단에 넣어야 되고,  basePackage의 속성값으로, 스캔하고자 하는 상위패키지의 이름을 넣어준다.

@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;
	}
}

이런식으로 말이다.

또한 이렇게 자동 등록한 빈을 getBean 메서드를 통해 꺼내쓸때는, @Component로 등록한 "정확한 빈 객체 식별자"를 파라미터로 넘겨줄 필요가 있다. 굳이 타입이 여러개가 아니면, 파라미터로 타입만 넘겨줘도 getBean이 알아서 찾아준다.

또한 빈 이름이 같은 경우 타입이 같으면, 충돌이 일어날수 있는데, 이럴경우에는 빈객체의 식별자를 알맞게 다시 설정해 주면 된다.

또한 빈 이름은 같고, 같은 타입이라면, 두 클래스 모두 빈객체에 등록되는데, 이럴경우에는 @Autowired를 통해 DI를 할때, 이전 포스트에서 작성한 @Qualifier 애너테이션을 통해 한정자를 다르게 바꿔주면 된다.

 

page 140 공부완료.

 

순공부/ 실습 시간 1시간.

 

오늘은 1시간동안 독학한 내용 정리를 영신에서 다 못하고, 스벅에서 1시간동안 복습하며 정리했다. 뿌듯하다.

하루하루가 쌓여갈수록 자존감도 올라가는 기분이다. 하루하루 작은 습관이 모여 큰 변화가 일어난다는 내 가치관을 잃지말자.